diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..cb5219f2
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,43 @@
+# Pull Request
+
+## 📌 Summary
+
+
+
+## 🎯 Type of change
+
+- [ ] Feature
+- [ ] Bug fix
+- [ ] Refactor
+- [ ] Performance
+- [ ] Documentation
+- [ ] Chore / Build / CI
+
+## 🔗 Related work
+
+
+
+## 🧪 How was this tested?
+
+
+
+- [ ] Unit tests
+- [ ] Integration tests
+- [ ] Manual tests
+- [ ] Not applicable
+
+## ✅ Checklist
+
+- [ ] I have reviewed my own changes
+- [ ] I have updated documentation/comments when needed
+- [ ] I have added or updated tests when needed
+- [ ] I have verified no sensitive data/secrets were introduced
+- [ ] This PR is ready for review
+
+## 📝 Additional notes (optional)
+
+
+
+## 🧾 Release notes (optional)
+
+
\ No newline at end of file
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index cf3cd8a6..4f082bbf 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -4,25 +4,38 @@ on:
workflow_dispatch:
jobs:
- validate:
+ validate-template-full:
+ name: Validate Full Template
uses: gpreviatti/github-actions-templates/.github/workflows/dotnet-validate.yml@v1
with:
- project: "hexagonal-solution-template"
- organization: "gpreviatti"
- unit_test_project_path: "./templates/tests/UnitTests/"
- unit_test_verbosity: 'd'
+ build_path: "./templates/Full"
+ project: "hexagonal-solution-template-full"
+ unit_test_project_path: "./templates/Full/tests/UnitTests/"
dotnet_version: "${{ vars.PROJECT_DOTNET_VERSION }}"
- domain_stryker_config_path: "./templates/tests/UnitTests/stryker-config-domain.json"
- application_stryker_config_path: "./templates/tests/UnitTests/stryker-config-application.json"
- integration_test_project_path: "./templates/tests/IntegrationTests"
- docker_compose_file_path: "./templates/docker-compose.yml"
+ domain_stryker_config_path: "./templates/Full/tests/UnitTests/stryker-config-domain.json"
+ application_stryker_config_path: "./templates/Full/tests/UnitTests/stryker-config-application.json"
+ integration_test_project_path: "./templates/Full/tests/IntegrationTests"
+ docker_compose_file_path: "./templates/Full/docker-compose.yml"
secrets:
sonar_token: ${{ secrets.SONAR_TOKEN }}
+
+ validate-template-bff:
+ name: Validate BFF Template
+ uses: gpreviatti/github-actions-templates/.github/workflows/dotnet-validate.yml@v1
+ with:
+ build_path: "./templates/Bff"
+ dotnet_version: "${{ vars.PROJECT_DOTNET_VERSION }}"
+ integration_test_project_path: "./templates/Bff/tests/IntegrationTests"
+ docker_compose_file_path: "./templates/Bff/docker-compose.yml"
+ secrets:
+ sonar_token: ${{ secrets.SONAR_TOKEN }}
+
pack-and-publish:
- needs: validate
+ name: Pack and Publish Template
+ needs: [validate-template-full, validate-template-bff]
uses: gpreviatti/github-actions-templates/.github/workflows/dotnet-pack.yml@v1
with:
dotnet_version: "${{ vars.PROJECT_DOTNET_VERSION }}"
- package_version: "10.0.3"
+ package_version: "10.1.0"
secrets:
nuget_api_key: ${{ secrets.NUGET_API_KEY }}
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 269e4843..5b0d7898 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -5,18 +5,27 @@ on:
workflow_dispatch:
jobs:
- validate:
+ validate-template-full:
+ name: Validate Full Template
uses: gpreviatti/github-actions-templates/.github/workflows/dotnet-validate.yml@v1
with:
- project: "hexagonal-solution-template"
- organization: "gpreviatti"
- unit_test_project_path: "./templates/tests/UnitTests/"
- unit_test_verbosity: 'd'
+ build_path: "./templates/Full"
+ sonar_project: "hexagonal-solution-template-full"
+ test_no_build: false
+ unit_test_project_path: "./templates/Full/tests/UnitTests/"
dotnet_version: "${{ vars.PROJECT_DOTNET_VERSION }}"
- domain_stryker_config_path: "./templates/tests/UnitTests/stryker-config-domain.json"
- application_stryker_config_path: "./templates/tests/UnitTests/stryker-config-application.json"
- integration_test_project_path: "./templates/tests/IntegrationTests"
- docker_compose_file_path: "./templates/docker-compose.yml"
+ domain_stryker_config_path: "./templates/Full/tests/UnitTests/stryker-config-domain.json"
+ application_stryker_config_path: "./templates/Full/tests/UnitTests/stryker-config-application.json"
+ integration_test_project_path: "./templates/Full/tests/IntegrationTests"
+ docker_compose_file_path: "./templates/Full/docker-compose.yml"
secrets:
sonar_token: ${{ secrets.SONAR_TOKEN }}
-
+
+ validate-template-bff:
+ name: Validate BFF Template
+ uses: gpreviatti/github-actions-templates/.github/workflows/dotnet-validate.yml@v1
+ with:
+ build_path: "./templates/Bff"
+ dotnet_version: "${{ vars.PROJECT_DOTNET_VERSION }}"
+ integration_test_project_path: "./templates/Bff/tests/IntegrationTests"
+ docker_compose_file_path: "./templates/Bff/docker-compose.yml"
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 00000000..a0fc763c
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,47 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Full [Development]",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ // If you have changed target frameworks, make sure to update the program path.
+ "program": "${workspaceFolder}/templates/Full/src/WebApp/bin/Debug/net10.0/WebApp.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}/templates/Full/src/WebApp",
+ "stopAtEntry": false,
+ "serverReadyAction": {
+ "action": "openExternally",
+ "pattern": "\\bNow listening on:\\s+(https?://\\S+)"
+ },
+ "env": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "sourceFileMap": {
+ "/Views": "${workspaceFolder}/Views"
+ }
+ },
+ {
+ "name": "Bff [Development]",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ // If you have changed target frameworks, make sure to update the program path.
+ "program": "${workspaceFolder}/templates/Bff/src/WebApp/bin/Debug/net10.0/WebApp.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}/templates/Bff/src/WebApp",
+ "stopAtEntry": false,
+ "serverReadyAction": {
+ "action": "openExternally",
+ "pattern": "\\bNow listening on:\\s+(https?://\\S+)"
+ },
+ "env": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "sourceFileMap": {
+ "/Views": "${workspaceFolder}/Views"
+ }
+ },
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 00000000..caabffa5
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,41 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "${workspaceFolder}/templates/Bff/src/WebApp/WebApp.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary;ForceNoAlign"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "publish",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "publish",
+ "${workspaceFolder}/templates/Bff/src/WebApp/WebApp.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary;ForceNoAlign"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "watch",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "watch",
+ "run",
+ "--project",
+ "${workspaceFolder}/templates/Bff/src/WebApp/WebApp.csproj"
+ ],
+ "problemMatcher": "$msCompile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/GPreviatti.Template.Hexagonal.Solution.csproj b/GPreviatti.Template.Hexagonal.Solution.csproj
index c78eba8d..844afca5 100644
--- a/GPreviatti.Template.Hexagonal.Solution.csproj
+++ b/GPreviatti.Template.Hexagonal.Solution.csproj
@@ -4,7 +4,7 @@
GPreviatti.Template.Hexagonal.Solution
Hexagonal architecture solution template
Giovanni Brunno Previatti
- Solution following Hexagonal architecture and best praticies.
+ Solution following Hexagonal architecture and best practices.
template;hexagonal-architecture;clean-architecture;ddd;net8
net10.0
true
@@ -18,10 +18,14 @@
git://github.com/gpreviatti/hexagonal-solution-template
true
- - Add docker support
- - Add docker-compose file for load tests
- - Add automatic assembly mapping for entities
- - Simplify load tests methods definitions
+ - Add new new template with BFF architecture
+ - Remove Repository pattern since it's not needed with modern EF Core and adds unnecessary complexity
+ - Update dependencies to latest versions
+ - Update GitHub Actions workflows to test and analyze both templates
+ - Improve OpenTelemetry configuration and add more metrics and traces
+ - Remove Aspire Dashboard
+ - Add Grafana, Prometheus, Tempo and Loki configuration with Docker Compose for easier local development and testing of observability features
+ - Change DB to PostgreSQL since it's more widely used in production than SQL Server and works better with Docker
diff --git a/templates/.editorconfig b/templates/Bff/.editorconfig
similarity index 100%
rename from templates/.editorconfig
rename to templates/Bff/.editorconfig
diff --git a/templates/.gitignore b/templates/Bff/.gitignore
similarity index 100%
rename from templates/.gitignore
rename to templates/Bff/.gitignore
diff --git a/templates/Bff/.template.config/template.json b/templates/Bff/.template.config/template.json
new file mode 100644
index 00000000..8ab61a0a
--- /dev/null
+++ b/templates/Bff/.template.config/template.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "http://json.schemastore.org/template",
+ "author": "Giovanni Brunno Previatti",
+ "identity": "Hexagonal.Solution.Template.Bff",
+ "name": "Hexagonal solution template Bff",
+ "description": "Solution template following hexagonal architecture structure best practices for backend for frontend (BFF) applications",
+ "shortName": "bff",
+ "sourceName": "Hexagonal.Solution.Template.Bff",
+ "classifications": [
+ "common",
+ "template",
+ "hexagonal-architecture"
+ ],
+ "tags": {
+ "language": "C#",
+ "type": "project"
+ },
+ "preferNameDirectory": true
+}
diff --git a/templates/Directory.Build.props b/templates/Bff/Directory.Build.props
similarity index 52%
rename from templates/Directory.Build.props
rename to templates/Bff/Directory.Build.props
index b46ddbca..5053742d 100644
--- a/templates/Directory.Build.props
+++ b/templates/Bff/Directory.Build.props
@@ -4,8 +4,9 @@
enable
enable
true
+ latest
+ Recommended
+ true
+ false
-
-
-
\ No newline at end of file
diff --git a/templates/Bff/Directory.Packages.props b/templates/Bff/Directory.Packages.props
new file mode 100644
index 00000000..7a7040ba
--- /dev/null
+++ b/templates/Bff/Directory.Packages.props
@@ -0,0 +1,46 @@
+
+
+ true
+ true
+ $(NoWarn);NU1507
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/Bff/Dockerfile b/templates/Bff/Dockerfile
new file mode 100644
index 00000000..9b7e6a8d
--- /dev/null
+++ b/templates/Bff/Dockerfile
@@ -0,0 +1,19 @@
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build-env
+WORKDIR /app
+
+COPY . .
+
+WORKDIR /app/src/WebApp
+RUN dotnet restore \
+ && dotnet publish -c Release -o /app/out
+
+FROM mcr.microsoft.com/dotnet/aspnet:10.0
+WORKDIR /app
+COPY --from=build-env /app/out .
+
+RUN apt-get update \
+ && apt-get install -y curl
+
+EXPOSE 5011
+
+ENTRYPOINT ["dotnet", "WebApp.dll", "--urls", "http://+:5011"]
diff --git a/templates/Bff/Dockerfile.MockApi b/templates/Bff/Dockerfile.MockApi
new file mode 100644
index 00000000..3fdacafd
--- /dev/null
+++ b/templates/Bff/Dockerfile.MockApi
@@ -0,0 +1,19 @@
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build-env
+WORKDIR /app
+
+COPY . .
+
+WORKDIR /app/src/MockApi
+RUN dotnet restore \
+ && dotnet publish -c Release -o /app/out
+
+FROM mcr.microsoft.com/dotnet/aspnet:10.0
+WORKDIR /app
+COPY --from=build-env /app/out .
+
+RUN apt-get update \
+ && apt-get install -y curl
+
+EXPOSE 5012
+
+ENTRYPOINT ["dotnet", "MockApi.dll", "--urls", "http://+:5012"]
diff --git a/templates/Bff/Hexagonal.Solution.Template.Bff.slnx b/templates/Bff/Hexagonal.Solution.Template.Bff.slnx
new file mode 100644
index 00000000..f46845ee
--- /dev/null
+++ b/templates/Bff/Hexagonal.Solution.Template.Bff.slnx
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/templates/Bff/Readme.md b/templates/Bff/Readme.md
new file mode 100644
index 00000000..08f55d2f
--- /dev/null
+++ b/templates/Bff/Readme.md
@@ -0,0 +1,52 @@
+# Hexagonal Architecture Solution Template
+
+This repository provides a template for building applications using the Hexagonal Architecture (also known as Ports and Adapters Architecture). The template includes a well-structured project layout, sample implementations, and best practices to help you get started quickly.
+
+## Project Structure
+
+- `src/`: Contains the source code for the application.
+ - `Infrastructure/`: Contains implementations for external systems (e.g., databases, messaging).
+ - `WebApp/`: Contains the web application layer (e.g., REST API, gRPC services).
+
+- `tests/`: Contains unit and integration tests for the application.
+ - `CommonTests/`: Contains shared test utilities and base classes.
+ - `IntegrationTests/`: Contains integration tests for the application.
+ - `LoadTests/`: Contains load tests for performance evaluation.
+
+## Helper Commands
+
+### Start docker compose
+
+```bash
+docker-compose up -d
+```
+
+### Run load tests with full summary for HTTP script
+
+```bash
+k6 run tests/LoadTests/scriptHttp.js --summary-mode=full
+```
+
+### Run load tests with full summary for gRPC script
+
+```bash
+k6 run tests/LoadTests/scriptGrpc.js --summary-mode=full
+```
+
+### Run tests (except load tests)
+
+```bash
+dotnet test
+```
+
+### Run unit tests
+
+```bash
+dotnet test tests/UnitTests
+```
+
+### Run integration tests
+
+```bash
+dotnet test tests/IntegrationTests
+```s
diff --git a/templates/Bff/docker-compose-load-tests.yml b/templates/Bff/docker-compose-load-tests.yml
new file mode 100644
index 00000000..fdb74a85
--- /dev/null
+++ b/templates/Bff/docker-compose-load-tests.yml
@@ -0,0 +1,85 @@
+name: hexagonal-solution-template-bff-load-tests
+
+services:
+ k6:
+ container_name: k6
+ image: grafana/k6:1.4.2
+ depends_on:
+ bff:
+ condition: service_healthy
+ ports:
+ - "6565:6565"
+ environment:
+ - WEBAPP_URL=http://bff:5011
+ - K6_SUMMARY_MODE=full
+ - K6_WEB_DASHBOARD=true
+ - K6_WEB_DASHBOARD_EXPORT=html-report.html
+ - VUS=50
+ - DURATION=180s
+ - GRACEFUL_STOP=10s
+ command: ["run", "/LoadTests/scriptHttp.js", ]
+ volumes:
+ - ./tests/LoadTests:/LoadTests
+
+ bff:
+ container_name: bff
+ build:
+ context: .
+ dockerfile: ./Dockerfile
+ ports:
+ - "5011:5011"
+ depends_on:
+ redis:
+ condition: service_healthy
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:5011/health" ]
+ interval: 30s
+ timeout: 10s
+ retries: 5
+ start_period: 15s
+ environment:
+ - ASPNETCORE_ENVIRONMENT=Development
+ - OTEL_SERVICE_NAME=Hexagonal.Solution.Template.Bff
+ - OTEL_EXPORTER_OTLP_ENDPOINT=http://alloy:4317
+ - OTEL_EXPORTER_OTLP_PROTOCOL=grpc
+ - OTEL_RESOURCE_ATTRIBUTES=service.namespace=load-tests
+
+ mock-api:
+ extends:
+ file: docker-compose.yml
+ service: mock-api
+
+ redis:
+ extends:
+ file: docker-compose.yml
+ service: redis
+
+ alloy:
+ extends:
+ file: docker-compose.yml
+ service: alloy
+
+ prometheus:
+ extends:
+ file: docker-compose.yml
+ service: prometheus
+
+ loki:
+ extends:
+ file: docker-compose.yml
+ service: loki
+
+ grafana:
+ extends:
+ file: docker-compose.yml
+ service: grafana
+
+ tempo-init:
+ extends:
+ file: docker-compose.yml
+ service: tempo-init
+
+ tempo:
+ extends:
+ file: docker-compose.yml
+ service: tempo
diff --git a/templates/Bff/docker-compose-local.yml b/templates/Bff/docker-compose-local.yml
new file mode 100644
index 00000000..dec4aaf4
--- /dev/null
+++ b/templates/Bff/docker-compose-local.yml
@@ -0,0 +1,102 @@
+name: hexagonal-solution-template-bff
+
+services:
+ mock-api:
+ container_name: mock-api
+ build:
+ context: .
+ dockerfile: Dockerfile.MockApi
+ ports:
+ - "5012:5012"
+ healthcheck:
+ test: ["CMD", "curl", "--http2-prior-knowledge", "-v", "http://localhost:5012/health"]
+ interval: 3s
+ timeout: 10s
+ retries: 5
+ start_period: 15s
+ environment:
+ - ASPNETCORE_ENVIRONMENT=Development
+ - OTEL_SERVICE_NAME=Hexagonal.Solution.Template.Bff.MockApi
+ - OTEL_EXPORTER_OTLP_ENDPOINT=http://alloy:4317
+ - OTEL_EXPORTER_OTLP_PROTOCOL=grpc
+
+ redis:
+ image: redis:8
+ container_name: redis
+ ports:
+ - "6379:6379"
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 3s
+ timeout: 10s
+ retries: 5
+ start_period: 15s
+
+ alloy:
+ container_name: alloy
+ image: grafana/alloy:v1.7.5
+ ports:
+ - 12345:12345
+ - 4318:4318
+ - 4317:4317
+ volumes:
+ - ./scripts/grafana/config.alloy:/etc/alloy/config.alloy
+ - /var/lib/docker/containers:/var/lib/docker/containers:ro
+ - /var/run/docker.sock:/var/run/docker.sock
+ command: run --server.http.listen-addr=0.0.0.0:12345 --storage.path=/var/lib/alloy/data /etc/alloy/config.alloy
+ depends_on:
+ - loki
+ - prometheus
+ - tempo
+
+ prometheus:
+ container_name: prometheus
+ image: prom/prometheus:v3.1.0
+ ports:
+ - 9090:9090
+ volumes:
+ - ./scripts/grafana/prometheus.yml:/etc/prometheus/prometheus.yml
+ command: --config.file=/etc/prometheus/prometheus.yml --web.enable-otlp-receiver --enable-feature=exemplar-storage
+
+ loki:
+ container_name: loki
+ image: grafana/loki:3.4.2
+ ports:
+ - "3100:3100"
+ volumes:
+ - ./scripts/grafana/loki-config.yaml:/etc/loki/local-config.yaml
+ command: -config.file=/etc/loki/local-config.yaml
+
+ grafana:
+ container_name: grafana
+ image: grafana/grafana:11.5.2
+ environment:
+ - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
+ - GF_AUTH_ANONYMOUS_ENABLED=true
+ - GF_AUTH_BASIC_ENABLED=false
+ - GF_FEATURE_TOGGLES_ENABLE=accessControlOnCall
+ ports:
+ - 3000:3000/tcp
+ volumes:
+ - ./scripts/grafana/datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
+
+ tempo-init:
+ container_name: tempo-init
+ image: &tempoImage grafana/tempo:2.7.1
+ user: root
+ entrypoint:
+ - "chown"
+ - "10001:10001"
+ - "/var/tempo"
+
+ tempo:
+ container_name: tempo
+ image: *tempoImage
+ command: [ "-config.file=/etc/tempo.yaml" ]
+ volumes:
+ - ./scripts/grafana/tempo.yaml:/etc/tempo.yaml
+ ports:
+ - "3200:3200" # tempo
+ depends_on:
+ - tempo-init
+ - redis
diff --git a/templates/Bff/docker-compose.yml b/templates/Bff/docker-compose.yml
new file mode 100644
index 00000000..be73b356
--- /dev/null
+++ b/templates/Bff/docker-compose.yml
@@ -0,0 +1,33 @@
+name: hexagonal-solution-template-bff
+
+services:
+ mock-api:
+ container_name: mock-api
+ build:
+ context: .
+ dockerfile: Dockerfile.MockApi
+ ports:
+ - "5012:5012"
+ healthcheck:
+ test: ["CMD", "curl", "--http2-prior-knowledge", "-v", "http://localhost:5012/health"]
+ interval: 3s
+ timeout: 10s
+ retries: 5
+ start_period: 15s
+ environment:
+ - ASPNETCORE_ENVIRONMENT=Development
+ - OTEL_SERVICE_NAME=Hexagonal.Solution.Template.Bff.MockApi
+ - OTEL_EXPORTER_OTLP_ENDPOINT=http://alloy:4317
+ - OTEL_EXPORTER_OTLP_PROTOCOL=grpc
+
+ redis:
+ image: redis:8
+ container_name: redis
+ ports:
+ - "6379:6379"
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 3s
+ timeout: 10s
+ retries: 5
+ start_period: 15s
diff --git a/templates/Bff/scripts/grafana/config.alloy b/templates/Bff/scripts/grafana/config.alloy
new file mode 100644
index 00000000..34d692e5
--- /dev/null
+++ b/templates/Bff/scripts/grafana/config.alloy
@@ -0,0 +1,45 @@
+otelcol.receiver.otlp "default" {
+ http {
+ endpoint = "0.0.0.0:4318"
+
+ }
+ grpc {
+ endpoint = "0.0.0.0:4317"
+ }
+
+ output {
+ metrics = [otelcol.exporter.otlphttp.metrics.input]
+ logs = [otelcol.exporter.otlphttp.logs.input]
+ traces = [otelcol.exporter.otlphttp.traces.input,otelcol.connector.servicegraph.default.input]
+ }
+}
+
+otelcol.connector.servicegraph "default" {
+ dimensions = ["http.method"]
+
+ output {
+ metrics = [otelcol.exporter.otlphttp.metrics.input]
+ }
+}
+
+otelcol.exporter.otlphttp "logs" {
+ client {
+ endpoint = "http://loki:3100/otlp"
+ }
+}
+
+otelcol.exporter.otlphttp "metrics" {
+ client {
+ endpoint = "http://prometheus:9090/api/v1/otlp"
+ }
+}
+
+otelcol.exporter.otlphttp "traces" {
+ client {
+ endpoint = "http://tempo:4318"
+ }
+}
+
+livedebugging {
+ enabled = true
+}
\ No newline at end of file
diff --git a/templates/Bff/scripts/grafana/datasources.yaml b/templates/Bff/scripts/grafana/datasources.yaml
new file mode 100644
index 00000000..b6e00255
--- /dev/null
+++ b/templates/Bff/scripts/grafana/datasources.yaml
@@ -0,0 +1,55 @@
+apiVersion: 1
+
+datasources:
+- name: Tempo
+ type: tempo
+ access: proxy
+ orgId: 1
+ url: http://tempo:3200
+ basicAuth: false
+ isDefault: true
+ version: 1
+ editable: false
+ apiVersion: 1
+ uid: tempo
+ jsonData:
+ httpMethod: GET
+ serviceMap:
+ datasourceUid: prometheus
+ tracesToLogsV2:
+ # Field with an internal link pointing to a logs data source in Grafana.
+ # datasourceUid value must match the uid value of the logs data source.
+ datasourceUid: 'Loki'
+ spanStartTimeShift: '-1h'
+ spanEndTimeShift: '1h'
+ filterByTraceID: true
+ filterBySpanID: true
+ customQuery: false
+ query: 'method="$${__span.tags.method}"'
+- name: Prometheus
+ type: prometheus
+ uid: prometheus
+ access: proxy
+ orgId: 1
+ url: http://prometheus:9090
+ basicAuth: false
+ isDefault: false
+ version: 1
+ editable: false
+ jsonData:
+ httpMethod: GET
+- name: Loki
+ type: loki
+ access: proxy
+ orgId: 1
+ url: http://loki:3100
+ basicAuth: false
+ isDefault: false
+ version: 1
+ editable: false
+ jsonData:
+ derivedFields:
+ - datasourceUid: tempo
+ matcherRegex: tid=(\w+)
+ name: TraceId
+ url: $${__value.raw}
diff --git a/templates/Bff/scripts/grafana/loki-config.yaml b/templates/Bff/scripts/grafana/loki-config.yaml
new file mode 100644
index 00000000..a9eca0ee
--- /dev/null
+++ b/templates/Bff/scripts/grafana/loki-config.yaml
@@ -0,0 +1,40 @@
+
+# This is a complete configuration to deploy Loki backed by the filesystem.
+# The index will be shipped to the storage via tsdb-shipper.
+
+auth_enabled: false
+
+limits_config:
+ allow_structured_metadata: true
+ volume_enabled: true
+
+server:
+ http_listen_port: 3100
+
+common:
+ ring:
+ instance_addr: 0.0.0.0
+ kvstore:
+ store: inmemory
+ replication_factor: 1
+ path_prefix: /tmp/loki
+
+schema_config:
+ configs:
+ - from: 2020-05-15
+ store: tsdb
+ object_store: filesystem
+ schema: v13
+ index:
+ prefix: index_
+ period: 24h
+
+storage_config:
+ tsdb_shipper:
+ active_index_directory: /tmp/loki/index
+ cache_location: /tmp/loki/index_cache
+ filesystem:
+ directory: /tmp/loki/chunks
+
+pattern_ingester:
+ enabled: true
\ No newline at end of file
diff --git a/templates/Bff/scripts/grafana/prometheus.yml b/templates/Bff/scripts/grafana/prometheus.yml
new file mode 100644
index 00000000..e71c1eb3
--- /dev/null
+++ b/templates/Bff/scripts/grafana/prometheus.yml
@@ -0,0 +1,7 @@
+global:
+ scrape_interval: 3s
+ evaluation_interval: 3s
+
+storage:
+ tsdb:
+ out_of_order_time_window: 30m
diff --git a/templates/Bff/scripts/grafana/tempo.yaml b/templates/Bff/scripts/grafana/tempo.yaml
new file mode 100644
index 00000000..94b3d42f
--- /dev/null
+++ b/templates/Bff/scripts/grafana/tempo.yaml
@@ -0,0 +1,89 @@
+stream_over_http_enabled: true
+server:
+ http_listen_port: 3200
+ log_level: info
+
+cache:
+ background:
+ writeback_goroutines: 5
+ caches:
+ - roles:
+ - frontend-search
+ redis:
+ endpoint: redis:6379
+
+query_frontend:
+ search:
+ duration_slo: 5s
+ throughput_bytes_slo: 1.073741824e+09
+ metadata_slo:
+ duration_slo: 5s
+ throughput_bytes_slo: 1.073741824e+09
+ trace_by_id:
+ duration_slo: 100ms
+ metrics:
+ max_duration: 120h # maximum duration of a metrics query, increase for local setups
+ query_backend_after: 5m
+ duration_slo: 5s
+ throughput_bytes_slo: 1.073741824e+09
+
+distributor:
+ receivers: # this configuration will listen on all ports and protocols that tempo is capable of.
+ jaeger: # the receives all come from the OpenTelemetry collector. more configuration information can
+ protocols: # be found there: https://github.com/open-telemetry/opentelemetry-collector/tree/main/receiver
+ thrift_http: #
+ endpoint: "tempo:14268" # for a production deployment you should only enable the receivers you need!
+ grpc:
+ endpoint: "tempo:14250"
+ thrift_binary:
+ endpoint: "tempo:6832"
+ thrift_compact:
+ endpoint: "tempo:6831"
+ zipkin:
+ endpoint: "tempo:9411"
+ otlp:
+ protocols:
+ grpc:
+ endpoint: "tempo:4317"
+ http:
+ endpoint: "tempo:4318"
+ opencensus:
+ endpoint: "tempo:55678"
+
+ingester:
+ max_block_duration: 5m # cut the headblock when this much time passes. this is being set for demo purposes and should probably be left alone normally
+
+compactor:
+ compaction:
+ block_retention: 24h # overall Tempo trace retention. set for demo purposes
+
+metrics_generator:
+ registry:
+ external_labels:
+ source: tempo
+ cluster: docker-compose
+ storage:
+ path: /var/tempo/generator/wal
+ remote_write:
+ - url: http://prometheus:9090/api/v1/write
+ send_exemplars: true
+ traces_storage:
+ path: /var/tempo/generator/traces
+ processor:
+ local_blocks:
+ filter_server_spans: false
+ flush_to_storage: true
+
+storage:
+ trace:
+ backend: local # backend configuration to use
+ wal:
+ path: /var/tempo/wal # where to store the wal locally
+ local:
+ path: /var/tempo/blocks
+
+overrides:
+ defaults:
+ metrics_generator:
+ processors: [service-graphs, span-metrics, local-blocks] # enables metrics generator
+ generate_native_histograms: both
\ No newline at end of file
diff --git a/templates/Bff/src/Contracts/Common/BaseRequest.cs b/templates/Bff/src/Contracts/Common/BaseRequest.cs
new file mode 100644
index 00000000..80440f48
--- /dev/null
+++ b/templates/Bff/src/Contracts/Common/BaseRequest.cs
@@ -0,0 +1,25 @@
+namespace Contracts.Common;
+
+///
+/// Base request structure
+///
+/// The unique identifier for correlating requests
+public record BaseRequest(Guid CorrelationId);
+
+///
+/// Base paginated request structure
+///
+/// The unique identifier for correlating requests
+/// The page number to retrieve
+/// The number of items per page
+/// The field to sort by
+/// Indicates whether the sorting is in descending order
+/// A dictionary of search criteria
+public record BasePaginatedRequest(
+ Guid CorrelationId,
+ int Page = 1,
+ int PageSize = 10,
+ string? SortBy = null,
+ bool SortDescending = false,
+ Dictionary? SearchByValues = null
+) : BaseRequest(CorrelationId);
\ No newline at end of file
diff --git a/templates/Bff/src/Contracts/Common/BaseResponse.cs b/templates/Bff/src/Contracts/Common/BaseResponse.cs
new file mode 100644
index 00000000..440f33c6
--- /dev/null
+++ b/templates/Bff/src/Contracts/Common/BaseResponse.cs
@@ -0,0 +1,99 @@
+namespace Contracts.Common;
+
+///
+/// Base response structure
+///
+public record BaseResponse
+{
+ ///
+ /// Base response structure
+ ///
+ public BaseResponse() { }
+
+ ///
+ /// Base response structure
+ ///
+ /// Indicates whether the response is successful
+ /// Optional message providing additional information
+ public BaseResponse(bool success, string? message = null)
+ {
+ Success = success;
+ Message = message;
+ }
+
+ ///
+ /// Indicates whether the response is successful
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// Optional message providing additional information
+ ///
+ public string? Message { get; set; }
+}
+
+///
+/// Generic base response structure with data
+///
+/// The type of the data included in the response
+public record BaseResponse : BaseResponse where TData : class
+{
+ ///
+ /// Generic base response structure with data
+ ///
+ public BaseResponse() { }
+
+ ///
+ /// Generic base response structure with data
+ ///
+ /// Indicates whether the response is successful
+ /// The data included in the response
+ /// Optional message providing additional information
+ public BaseResponse(bool success, TData? data = null, string? message = null) : base(success, message) => Data = data;
+
+ ///
+ /// The data included in the response
+ ///
+ public TData? Data { get; set; }
+
+}
+
+///
+/// Generic base paginated response structure with data
+///
+///
+public sealed record BasePaginatedResponse : BaseResponse>
+{
+ ///
+ /// Generic base paginated response structure with data
+ ///
+ public BasePaginatedResponse() { }
+
+ ///
+ /// Generic base paginated response structure with data
+ ///
+ /// Indicates whether the response is successful
+ /// The total number of pages available
+ /// The total number of records available
+ /// The data included in the response
+ /// Optional message providing additional information
+ public BasePaginatedResponse(
+ bool success, int totalPages, int totalRecords,
+ IEnumerable? data = null, string? message = null
+ ) : base(success, data, message)
+ {
+ TotalPages = totalPages;
+ TotalRecords = totalRecords;
+ }
+
+ ///
+ /// The total number of pages available
+ ///
+ public int TotalPages { get; set; }
+
+ ///
+ /// The total number of records available
+ ///
+ public int TotalRecords { get; set; }
+
+}
diff --git a/templates/Bff/src/Contracts/Contracts.csproj b/templates/Bff/src/Contracts/Contracts.csproj
new file mode 100644
index 00000000..be735abb
--- /dev/null
+++ b/templates/Bff/src/Contracts/Contracts.csproj
@@ -0,0 +1,11 @@
+
+
+ true
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/Bff/src/Contracts/Orders/CreateOrderRequest.cs b/templates/Bff/src/Contracts/Orders/CreateOrderRequest.cs
new file mode 100644
index 00000000..3a67a0da
--- /dev/null
+++ b/templates/Bff/src/Contracts/Orders/CreateOrderRequest.cs
@@ -0,0 +1,23 @@
+using Contracts.Common;
+
+namespace Contracts.Orders;
+
+///
+/// Request to create a new order
+///
+/// The unique identifier for the request
+/// A description of the order
+/// The items included in the order
+public sealed record CreateOrderRequest(
+ Guid CorrelationId,
+ string Description,
+ CreateOrderItemRequest[] Items
+) : BaseRequest(CorrelationId);
+
+///
+/// An item included in the order
+///
+/// The name of the item
+/// A description of the item
+/// The value of the item
+public sealed record CreateOrderItemRequest(string Name, string Description, decimal Value);
\ No newline at end of file
diff --git a/templates/Bff/src/Contracts/Orders/OrderDto.cs b/templates/Bff/src/Contracts/Orders/OrderDto.cs
new file mode 100644
index 00000000..591c252e
--- /dev/null
+++ b/templates/Bff/src/Contracts/Orders/OrderDto.cs
@@ -0,0 +1,55 @@
+namespace Contracts.Orders;
+
+///
+/// Data transfer object representing an order
+///
+public sealed record OrderDto
+{
+ ///
+ /// The unique identifier of the order
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// A description of the order
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// The total value of the order
+ ///
+ /// 99.99
+ public decimal Total { get; set; }
+
+ ///
+ /// The items included in the order
+ ///
+ public IReadOnlyCollection? Items { get; set; }
+};
+
+///
+/// Data transfer object representing an item in an order
+///
+public sealed record ItemDto
+{
+ ///
+ /// The unique identifier of the item
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// The name of the item
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// A description of the item
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// The value of the item
+ ///
+ /// 99.99
+ public decimal Value { get; set; }
+};
diff --git a/templates/Bff/src/Contracts/Protos/payment.proto b/templates/Bff/src/Contracts/Protos/payment.proto
new file mode 100644
index 00000000..17d64541
--- /dev/null
+++ b/templates/Bff/src/Contracts/Protos/payment.proto
@@ -0,0 +1,33 @@
+syntax = "proto3";
+
+option csharp_namespace = "GrpcPayment";
+
+package Protos;
+
+// Service for managing payments
+service PaymentService {
+ // Creates a payment request for an order
+ rpc Create (CreatePaymentRequest) returns (PaymentReply);
+}
+
+// Request message for creating a payment
+message CreatePaymentRequest {
+ // Unique identifier for the order
+ int32 order_id = 1;
+ // Amount to be paid
+ double amount = 2;
+ // Correlation ID for tracking requests
+ string correlation_id = 3;
+ // Currency of the payment
+ string currency = 4;
+ // Method of payment
+ string payment_method = 5;
+}
+
+// Reply message for payment processing
+message PaymentReply {
+ // Indicates if the payment was successful
+ bool success = 1;
+ // Message providing additional information
+ string message = 2;
+}
\ No newline at end of file
diff --git a/templates/Bff/src/Contracts/packages.lock.json b/templates/Bff/src/Contracts/packages.lock.json
new file mode 100644
index 00000000..2a36cb88
--- /dev/null
+++ b/templates/Bff/src/Contracts/packages.lock.json
@@ -0,0 +1,192 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "Grpc.AspNetCore": {
+ "type": "Direct",
+ "requested": "[2.76.0, )",
+ "resolved": "2.76.0",
+ "contentHash": "LyXMmpN2Ba0TE35SOLSKbGqIYtJuhc1UgiaGfoW1X8KJERV70QI5KGW+ckEY7MrXoFWN/uWo4B70siVhbDmCgQ==",
+ "dependencies": {
+ "Google.Protobuf": "3.31.1",
+ "Grpc.AspNetCore.Server.ClientFactory": "2.76.0",
+ "Grpc.Tools": "2.76.0"
+ }
+ },
+ "Google.Protobuf": {
+ "type": "Transitive",
+ "resolved": "3.31.1",
+ "contentHash": "gSnJbUmGiOTdWddPhqzrEscHq9Ls6sqRDPB9WptckyjTUyx70JOOAaDLkFff8gManZNN3hllQ4aQInnQyq/Z/A=="
+ },
+ "Grpc.AspNetCore.Server": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "diSC/ZeNdSdxHdYSOpYwuSBBDYpuNVtJQFJfiBB0WrYOQ4lVMmdxuUZJcViahQyo8pCvS3Mueo5lqFxwwMF/iw==",
+ "dependencies": {
+ "Grpc.Net.Common": "2.76.0"
+ }
+ },
+ "Grpc.AspNetCore.Server.ClientFactory": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "y5KGO1GO0N2L/hCCMR05mmoK8j+v8rKvZ+9nothAxKx2Tf2CwV8f4TM5K0GkKfDsp4vrc4lm90MU6E+DeN7YIw==",
+ "dependencies": {
+ "Grpc.AspNetCore.Server": "2.76.0",
+ "Grpc.Net.ClientFactory": "2.76.0"
+ }
+ },
+ "Grpc.Core.Api": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "cSxC2tdnFdXXuBgIn1pjc4YBx7LXTCp4M0qn+SMBS35VWZY+cEQYLWTBDDhdBH1HzU7BV+ncVZlniGQHMpRJKQ=="
+ },
+ "Grpc.Net.Client": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "K1oldmqw2+Gn69nGRzZLhqSiUZwelX1GrBu/cUl9wNf1C0uB61vFS6JcxUUv9P8VoUJhFsmV44JA6lI2EUt4xw==",
+ "dependencies": {
+ "Grpc.Net.Common": "2.76.0",
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.0"
+ }
+ },
+ "Grpc.Net.ClientFactory": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "XI+kO69L9AV8B9N0UQOmH911r6MOEp9huHiavEsY56DJYuzJ9KAxNGy37dpV6CLbgCaN2uKmpOsZ9Pao6bmpVQ==",
+ "dependencies": {
+ "Grpc.Net.Client": "2.76.0",
+ "Microsoft.Extensions.Http": "8.0.0"
+ }
+ },
+ "Grpc.Net.Common": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "bZpiMVYgvpB44/wBh1RotrkqC7bg2FOasLri2GhR3hMKyzsiTxCoDE49YjPrJeFc4RW0wS8u+EInI09sjxVFRA==",
+ "dependencies": {
+ "Grpc.Core.Api": "2.76.0"
+ }
+ },
+ "Grpc.Tools": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "goRzYZVMgQtyLvkWdgTnPycg49hlNxnMkqGGgR3l7nCOm0bUh0YeAneiJ9JFk3XLgF4suQUdETYkl2Mg/TBr0w=="
+ },
+ "Microsoft.Extensions.Configuration": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA=="
+ },
+ "Microsoft.Extensions.Diagnostics": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "8.0.0",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0",
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.Diagnostics.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
+ "Microsoft.Extensions.Options": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.Http": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
+ "Microsoft.Extensions.Diagnostics": "8.0.0",
+ "Microsoft.Extensions.Logging": "8.0.0",
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.0",
+ "Microsoft.Extensions.Options": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Options": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Options.ConfigurationExtensions": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
+ "Microsoft.Extensions.Configuration.Binder": "8.0.0",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
+ "Microsoft.Extensions.Options": "8.0.0",
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g=="
+ },
+ "Microsoft.Extensions.DependencyInjection": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Logging": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/templates/Bff/src/Infrastructure/Cache/HybridCacheService.cs b/templates/Bff/src/Infrastructure/Cache/HybridCacheService.cs
new file mode 100644
index 00000000..2c038eed
--- /dev/null
+++ b/templates/Bff/src/Infrastructure/Cache/HybridCacheService.cs
@@ -0,0 +1,58 @@
+using System.Diagnostics;
+using Infrastructure.Common;
+using Microsoft.Extensions.Caching.Hybrid;
+using Microsoft.Extensions.Logging;
+
+namespace Infrastructure.Cache;
+
+public sealed class HybridCacheService(HybridCache cache, ILogger logger)
+{
+ private readonly HybridCache _cache = cache;
+ private readonly ILogger _logger = logger;
+ private readonly string _className = nameof(HybridCacheService);
+ private readonly ActivitySource _activities = DefaultConfigurations.ActivitySource;
+ public async ValueTask GetOrCreateAsync(
+ string key,
+ Func> factory,
+ CancellationToken cancellationToken
+ )
+ {
+ using var activity = _activities.StartActivity($"{_className}.{nameof(GetOrCreateAsync)}");
+
+ Logs.DebugStartingOperation(_logger, $"Key: {key}");
+
+ var result = await _cache.GetOrCreateAsync($"{DefaultConfigurations.ApplicationName}:{key}", factory, cancellationToken: cancellationToken);
+
+ Logs.DebugFinishedOperation(_logger, $"Cache hit: {result != null} for key: {key}");
+
+ activity?.SetTag("key", key);
+
+ return result;
+ }
+
+ public async ValueTask CreateAsync(string key, TResult value, CancellationToken cancellationToken)
+ {
+ using var activity = _activities.StartActivity($"{_className}.{nameof(CreateAsync)}");
+
+ Logs.DebugStartingOperation(_logger, $"Key: {key}");
+
+ await _cache.SetAsync($"{DefaultConfigurations.ApplicationName}:{key}", value, cancellationToken: cancellationToken);
+
+ Logs.DebugFinishedOperation(_logger, $"Cached hit: {value != null} for key: {key}");
+
+ activity?.SetTag("key", key);
+ }
+
+ public async ValueTask DeleteAsync(string key, CancellationToken cancellationToken)
+ {
+ using var activity = _activities.StartActivity($"{_className}.{nameof(DeleteAsync)}");
+
+ Logs.DebugStartingOperation(_logger, $"Key: {key}");
+
+ await _cache.RemoveAsync($"{DefaultConfigurations.ApplicationName}:{key}", cancellationToken);
+
+ Logs.DebugFinishedOperation(_logger, $"Cache entry removed for key: {key}");
+
+ activity?.SetTag("key", key);
+ }
+}
diff --git a/templates/Bff/src/Infrastructure/Common/DefaultConfigurations.cs b/templates/Bff/src/Infrastructure/Common/DefaultConfigurations.cs
new file mode 100644
index 00000000..6cef19f9
--- /dev/null
+++ b/templates/Bff/src/Infrastructure/Common/DefaultConfigurations.cs
@@ -0,0 +1,12 @@
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+
+namespace Infrastructure.Common;
+
+public static class DefaultConfigurations
+{
+ public static string ApplicationName => "Hexagonal.Solution.Template.Bff";
+ public static string Version => typeof(DefaultConfigurations).Assembly.GetName().Version!.ToString();
+ public static readonly Meter Meter = new(ApplicationName, Version);
+ public static readonly ActivitySource ActivitySource = new(ApplicationName, Version);
+}
diff --git a/templates/Bff/src/Infrastructure/Common/Logs.cs b/templates/Bff/src/Infrastructure/Common/Logs.cs
new file mode 100644
index 00000000..116185d9
--- /dev/null
+++ b/templates/Bff/src/Infrastructure/Common/Logs.cs
@@ -0,0 +1,122 @@
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.Logging;
+
+namespace Infrastructure.Common;
+
+public partial class Logs
+{
+ ///
+ /// Logs a generic debug message with a custom message.
+ ///
+ /// The logger instance to use for logging.
+ /// The debug message.
+ /// The name of the method (auto-captured).
+ [LoggerMessage(
+ EventId = 1,
+ Level = LogLevel.Debug,
+ Message = "[{MethodName}] | {Message}"
+ )]
+ public static partial void Debug(ILogger logger, string message, [CallerMemberName] string methodName = null!);
+
+ ///
+ /// Logs a generic information message with a custom message.
+ ///
+ /// The logger instance to use for logging.
+ /// The information message.
+ /// The name of the method (auto-captured).
+ [LoggerMessage(
+ EventId = 2,
+ Level = LogLevel.Information,
+ Message = "[{MethodName}] | {Message}"
+ )]
+ public static partial void Information(ILogger logger, string message, [CallerMemberName] string methodName = null!);
+
+ ///
+ /// Logs a generic warning message with a custom message.
+ ///
+ /// The logger instance to use for logging.
+ /// The warning message.
+ /// The name of the method (auto-captured).
+ [LoggerMessage(
+ EventId = 3,
+ Level = LogLevel.Warning,
+ Message = "[{MethodName}] | {Message}"
+ )]
+ public static partial void Warning(ILogger logger, string message, [CallerMemberName] string methodName = null!);
+
+ ///
+ /// Logs a generic operation failure with a custom message.
+ ///
+ /// The logger instance to use for logging.
+ /// The failure message.
+ /// The name of the method (auto-captured).
+ [LoggerMessage(
+ EventId = 4,
+ Level = LogLevel.Error,
+ Message = "[{MethodName}] | Error: {Message}"
+ )]
+ public static partial void Error(ILogger logger, string message, [CallerMemberName] string methodName = null!);
+
+ ///
+ /// Logs the start of the execution of an operation, including the method name and correlation ID.
+ ///
+ /// The logger instance to use for logging.
+ /// The name of the method where the operation is executed (auto-captured).
+ [LoggerMessage(
+ EventId = 5,
+ Level = LogLevel.Information,
+ Message = "[{MethodName}] | Starting operation"
+ )]
+ public static partial void StartingOperation(ILogger logger, [CallerMemberName] string methodName = null!);
+
+ ///
+ /// Logs the completion of the execution of an operation, including the method name and correlation ID.
+ ///
+ /// The logger instance to use for logging.
+ /// The name of the method where the operation is executed (auto-captured).
+ [LoggerMessage(
+ EventId = 6,
+ Level = LogLevel.Information,
+ Message = "[{MethodName}] | Finished operation"
+ )]
+ public static partial void FinishedOperation(ILogger logger, [CallerMemberName] string methodName = null!);
+
+ ///
+ /// Logs a generic operation failure with a custom message.
+ ///
+ /// The logger instance to use for logging.
+ /// The failure message.
+ /// The name of the method (auto-captured).
+ [LoggerMessage(
+ EventId = 7,
+ Level = LogLevel.Warning,
+ Message = "[{MethodName}] | Failed operation: {Message}"
+ )]
+ public static partial void FailedOperation(ILogger logger, string message, [CallerMemberName] string methodName = null!);
+
+ ///
+ /// Logs the start of an operation.
+ ///
+ /// The logger instance to use for logging.
+ /// Optional details about the operation.
+ /// The method name (auto-captured).
+ [LoggerMessage(
+ EventId = 8,
+ Level = LogLevel.Debug,
+ Message = "[{Method}] | Starting operation. | {Details}"
+ )]
+ public static partial void DebugStartingOperation(ILogger logger, string details = "", [CallerMemberName] string method = null!);
+
+ ///
+ /// Logs the completion of an operation.
+ ///
+ /// The logger instance to use for logging.
+ /// Optional details about the operation.
+ /// The method name (auto-captured).
+ [LoggerMessage(
+ EventId = 9,
+ Level = LogLevel.Debug,
+ Message = "[{Method}] | Finished operation. | {Details}"
+ )]
+ public static partial void DebugFinishedOperation(ILogger logger, string details = "", [CallerMemberName] string method = null!);
+}
\ No newline at end of file
diff --git a/templates/Bff/src/Infrastructure/Common/ServiceConfiguration.cs b/templates/Bff/src/Infrastructure/Common/ServiceConfiguration.cs
new file mode 100644
index 00000000..7ccc93a0
--- /dev/null
+++ b/templates/Bff/src/Infrastructure/Common/ServiceConfiguration.cs
@@ -0,0 +1,11 @@
+namespace Infrastructure.Common;
+
+public sealed class ServiceConfiguration
+{
+ public string Name { get; set; }
+ public string BaseAddress { get; set; }
+ public object Authentication { get; set; }
+ public object Headers { get; set; }
+ public int LimitPerMinute { get; set; }
+ public int ProtocolVersion { get; set; }
+}
\ No newline at end of file
diff --git a/templates/Bff/src/Infrastructure/Common/ServicesKey.cs b/templates/Bff/src/Infrastructure/Common/ServicesKey.cs
new file mode 100644
index 00000000..d9e05955
--- /dev/null
+++ b/templates/Bff/src/Infrastructure/Common/ServicesKey.cs
@@ -0,0 +1,7 @@
+namespace Infrastructure.Common;
+
+public enum ServicesKey
+{
+ Orders,
+ Payments
+}
diff --git a/templates/Bff/src/Infrastructure/Grpc/BaseGrpcService.cs b/templates/Bff/src/Infrastructure/Grpc/BaseGrpcService.cs
new file mode 100644
index 00000000..bee26efa
--- /dev/null
+++ b/templates/Bff/src/Infrastructure/Grpc/BaseGrpcService.cs
@@ -0,0 +1,74 @@
+using System.Runtime.CompilerServices;
+using Grpc.Core;
+using Grpc.Net.ClientFactory;
+using Infrastructure.Common;
+using Microsoft.Extensions.Logging;
+
+namespace Infrastructure.Grpc;
+
+public partial class BaseGrpcService where TGrpcService : ClientBase
+{
+ ///
+ /// gRPC client instance
+ ///
+ protected TGrpcService Client { get; }
+ ///
+ /// Logger instance for logging
+ ///
+ public ILogger Logger { get; }
+ ///
+ /// Class name of the derived gRPC service
+ ///
+ public string ClassName { get; }
+
+ ///
+ /// Base gRPC service constructor
+ ///
+ /// logger instance for logging
+ /// gRPC client factory for creating gRPC clients
+ public BaseGrpcService(ILogger logger, GrpcClientFactory grpcClientFactory)
+ {
+ var classType = GetType();
+ ClassName = classType.Name;
+ Logger = logger;
+ Client = grpcClientFactory.CreateClient(ClassName);
+ }
+
+ ///
+ /// Executes a gRPC handler with logging and error handling
+ ///
+ /// TResponse type returned by the gRPC handler
+ /// handler function representing the gRPC call
+ /// name of the calling method (automatically provided)
+ /// Task representing the asynchronous operation, containing the gRPC response
+ protected async Task ExecuteHandlerAsync(
+ Func> handler,
+ [CallerMemberName] string? methodName = null
+ ) where TResponse : class
+ {
+ DefaultConfigurations.ActivitySource.StartActivity($"{ClassName}.{methodName}");
+
+ try
+ {
+ Logs.Information(Logger, "Sending request");
+
+ var response = handler.Invoke();
+
+ Logs.Information(Logger, "Request completed");
+ return await response.ResponseAsync;
+ }
+ catch (RpcException rpcEx)
+ {
+ Logs.Error(Logger, $"Request failed: {rpcEx.Message}");
+
+ throw;
+ }
+ catch (Exception ex)
+ {
+ Logs.Error(Logger, $"Request failed: {ex.Message}");
+
+ throw;
+ }
+ }
+}
+
diff --git a/templates/Bff/src/Infrastructure/Grpc/PaymentsService.cs b/templates/Bff/src/Infrastructure/Grpc/PaymentsService.cs
new file mode 100644
index 00000000..ef1e01f2
--- /dev/null
+++ b/templates/Bff/src/Infrastructure/Grpc/PaymentsService.cs
@@ -0,0 +1,11 @@
+using Grpc.Net.ClientFactory;
+using GrpcPayment;
+using Microsoft.Extensions.Logging;
+
+namespace Infrastructure.Grpc;
+
+public sealed class PaymentsService(ILogger logger, GrpcClientFactory grpcClientFactory) : BaseGrpcService(logger, grpcClientFactory)
+{
+ public async Task CreatePaymentAsync(CreatePaymentRequest request) =>
+ await ExecuteHandlerAsync(() => Client.CreateAsync(request));
+}
diff --git a/templates/Bff/src/Infrastructure/Http/BaseHttpService.cs b/templates/Bff/src/Infrastructure/Http/BaseHttpService.cs
new file mode 100644
index 00000000..79588ee2
--- /dev/null
+++ b/templates/Bff/src/Infrastructure/Http/BaseHttpService.cs
@@ -0,0 +1,105 @@
+using System.Net;
+using System.Text.Json;
+using Infrastructure.Common;
+using Microsoft.Extensions.Logging;
+
+namespace Infrastructure.Http;
+
+public class BaseHttpService(HttpClient httpClient, ILogger logger, int httpProtocolVersion = 2)
+{
+ public HttpClient HttpClient { get; } = httpClient;
+ public ILogger Logger { get; } = logger;
+ public int HttpProtocolVersion { get; } = httpProtocolVersion;
+ public JsonSerializerOptions JsonSerializerOptions { get; } = new(JsonSerializerDefaults.Web);
+
+ private static Version GetHttpVersion(int httpVersion) => httpVersion switch
+ {
+ 1 => HttpVersion.Version11,
+ 3 => HttpVersion.Version30,
+ 2 or _ => HttpVersion.Version20
+ };
+
+ public async Task SendAsync(
+ string requestUri,
+ HttpMethod httpMethod,
+ TRequest request,
+ Dictionary? headers = null,
+ string contentType = "application/json",
+ CancellationToken cancellationToken = default
+ ) where TRequest : class where TResponse : class
+ {
+ DefaultConfigurations.ActivitySource.StartActivity($"{nameof(BaseHttpService)}.{nameof(SendAsync)}");
+
+ Logs.StartingOperation(Logger);
+
+ HttpRequestMessage requestMessage = new(httpMethod, requestUri)
+ {
+ Version = GetHttpVersion(HttpProtocolVersion),
+ VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher
+ };
+
+ using MemoryStream memoryStream = new();
+ await JsonSerializer.SerializeAsync(memoryStream, request, JsonSerializerOptions, cancellationToken);
+
+ memoryStream.Seek(0, SeekOrigin.Begin);
+ using var requestContent = new StreamContent(memoryStream);
+ requestContent.Headers.ContentType = new(contentType);
+ requestMessage.Content = requestContent;
+
+ if (headers != null) foreach (var header in headers)
+ requestMessage.Headers.Add(header.Key, header.Value);
+
+ using var response = await HttpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ Logs.FailedOperation(Logger, $"{httpMethod} {requestUri} failed: {response.StatusCode} - {response.ReasonPhrase}");
+ return null;
+ }
+
+ var content = await response.Content.ReadAsStreamAsync(cancellationToken);
+
+ var result = await JsonSerializer.DeserializeAsync(content, JsonSerializerOptions, cancellationToken);
+
+ Logs.FinishedOperation(Logger);
+
+ return result;
+ }
+
+ public async Task SendAsync(
+ string requestUri,
+ HttpMethod httpMethod,
+ CancellationToken cancellationToken,
+ Dictionary? headers = null
+ ) where TResponse : class
+ {
+ DefaultConfigurations.ActivitySource.StartActivity($"{nameof(BaseHttpService)}.{nameof(SendAsync)}");
+
+ Logs.StartingOperation(Logger);
+
+ var requestMessage = new HttpRequestMessage(httpMethod, requestUri)
+ {
+ Version = GetHttpVersion(HttpProtocolVersion),
+ VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher
+ };
+
+ if (headers != null) foreach (var header in headers)
+ requestMessage.Headers.Add(header.Key, header.Value);
+
+ using var response = await HttpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ Logs.FailedOperation(Logger, $"{httpMethod} {requestUri} failed: {response.StatusCode} - {response.ReasonPhrase}");
+ return null;
+ }
+
+ var content = await response.Content.ReadAsStreamAsync(cancellationToken);
+
+ var result = await JsonSerializer.DeserializeAsync(content, JsonSerializerOptions, cancellationToken);
+
+ Logs.FinishedOperation(Logger);
+
+ return result;
+ }
+}
diff --git a/templates/Bff/src/Infrastructure/Infrastructure.csproj b/templates/Bff/src/Infrastructure/Infrastructure.csproj
new file mode 100644
index 00000000..4a4db540
--- /dev/null
+++ b/templates/Bff/src/Infrastructure/Infrastructure.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/Bff/src/Infrastructure/InfrastructureDependencyInjection.cs b/templates/Bff/src/Infrastructure/InfrastructureDependencyInjection.cs
new file mode 100644
index 00000000..2e838a6c
--- /dev/null
+++ b/templates/Bff/src/Infrastructure/InfrastructureDependencyInjection.cs
@@ -0,0 +1,176 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
+using OpenTelemetry.Logs;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Configuration;
+using Infrastructure.Cache;
+using Infrastructure.Http;
+using Polly;
+using Polly.Extensions.Http;
+using Infrastructure.Grpc;
+using Infrastructure.Common;
+using GrpcPayment;
+
+namespace Infrastructure;
+
+#pragma warning disable CA1708 // Identifiers should differ by more than case
+public static class InfrastructureDependencyInjection
+#pragma warning restore CA1708 // Identifiers should differ by more than case
+{
+ extension(WebApplicationBuilder builder)
+ {
+ public WebApplicationBuilder AddInfrastructure()
+ {
+ var configuration = builder.Configuration;
+
+ var serviceConfiguration = configuration.GetSection("Services").Get>()
+ ?? throw new ArgumentNullException(configuration.GetSection("Services").Path, "Services configuration is not configured.");
+
+ builder.Services
+ .AddCache(configuration)
+ .AddHttp(serviceConfiguration)
+ .AddGrpc(serviceConfiguration);
+
+ builder.AddOpenTelemetry();
+
+ return builder;
+ }
+
+ internal WebApplicationBuilder AddOpenTelemetry()
+ {
+ var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
+ if (string.Equals(environment, "IntegrationTests", StringComparison.OrdinalIgnoreCase))
+ return builder;
+
+ var serviceName = DefaultConfigurations.ApplicationName;
+ var serviceVersion = DefaultConfigurations.Version;
+ var resourceBuilder = ResourceBuilder
+ .CreateDefault()
+ .AddService(serviceName, serviceVersion: serviceVersion);
+
+ builder.Services.AddOpenTelemetry()
+ .WithMetrics(metrics => metrics
+ .SetResourceBuilder(resourceBuilder)
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation()
+ .AddProcessInstrumentation()
+ .AddOtlpExporter()
+ )
+ .WithTracing(tracing => tracing
+ .AddSource(serviceName)
+ .SetResourceBuilder(resourceBuilder)
+ .AddRedisInstrumentation()
+ .AddGrpcClientInstrumentation()
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddOtlpExporter()
+ );
+
+ builder.Logging.AddOpenTelemetry(options =>
+ {
+ options.IncludeFormattedMessage = true;
+ options.IncludeScopes = true;
+ options.ParseStateValues = true;
+ options
+ .SetResourceBuilder(resourceBuilder)
+ .AttachLogsToActivityEvent()
+ .AddOtlpExporter();
+ });
+
+ return builder;
+ }
+ }
+
+ extension(IServiceCollection services)
+ {
+ internal IServiceCollection AddCache(IConfiguration configuration)
+ {
+ services
+ .AddStackExchangeRedisCache(options =>
+ {
+ options.Configuration = configuration.GetConnectionString("Redis") ?? throw new ArgumentNullException(configuration.GetConnectionString("Redis"),"Redis connection string is not configured.");
+ options.Configuration += ",abortConnect=false,connectTimeout=5000,syncTimeout=5000";
+ })
+ .AddHybridCache(options =>
+ {
+ options.DefaultEntryOptions = new()
+ {
+ Expiration = TimeSpan.FromMinutes(30),
+ LocalCacheExpiration = TimeSpan.FromMinutes(30)
+ };
+ });
+
+ services.AddSingleton();
+
+ return services;
+ }
+
+ internal IServiceCollection AddHttp(List serviceConfiguration)
+ {
+
+ var serviceKeys = Enum.GetValues();
+
+ foreach (var serviceKey in serviceKeys)
+ {
+ var serviceName = serviceKey.ToString();
+
+ var serviceConfig = serviceConfiguration.FirstOrDefault(x =>
+ string.Equals(x.Name, serviceName, StringComparison.OrdinalIgnoreCase))
+ ?? throw new ArgumentNullException($"{serviceName} service configuration is not configured.");
+
+ services.AddHttpClient(serviceName, client =>
+ {
+ client.BaseAddress = new Uri(serviceConfig.BaseAddress)
+ ?? throw new ArgumentNullException($"{serviceName} service address is not configured.");
+
+ client.DefaultRequestVersion = new(serviceConfig.ProtocolVersion, 0);
+
+ if (serviceConfig.Headers is Dictionary headers && headers.Count > 0)
+ foreach (var header in headers)
+ client.DefaultRequestHeaders.Add(header.Key, header.Value);
+ })
+ .SetHandlerLifetime(TimeSpan.FromMinutes(5))
+ .AddPolicyHandler(GetRetryPolicy());
+
+ services.AddKeyedScoped(serviceKey, (serviceProvider, _) =>
+ {
+ var httpClientFactory = serviceProvider.GetRequiredService();
+ var logger = serviceProvider.GetRequiredService>();
+ var client = httpClientFactory.CreateClient(serviceName);
+
+ return new(client, logger, serviceConfig.ProtocolVersion);
+ });
+ }
+
+ return services;
+ }
+
+ internal IServiceCollection AddGrpc(List serviceConfiguration)
+ {
+ services.AddGrpc();
+ services
+ .AddGrpcClient(nameof(PaymentsService), o =>
+ {
+ var paymentsConfiguration = serviceConfiguration.FirstOrDefault(x =>
+ string.Equals(x.Name, ServicesKey.Payments.ToString(), StringComparison.OrdinalIgnoreCase))
+ ?? throw new ArgumentNullException($"{ServicesKey.Payments} gRPC service configuration is not configured.");
+
+ o.Address = new Uri(paymentsConfiguration.BaseAddress);
+ })
+ .SetHandlerLifetime(TimeSpan.FromMinutes(5))
+ .AddPolicyHandler(GetRetryPolicy());
+
+ services.AddScoped();
+
+ return services;
+ }
+
+ internal static IAsyncPolicy GetRetryPolicy() => HttpPolicyExtensions
+ .HandleTransientHttpError()
+ .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
+ }
+}
diff --git a/templates/Bff/src/Infrastructure/packages.lock.json b/templates/Bff/src/Infrastructure/packages.lock.json
new file mode 100644
index 00000000..4d967e3b
--- /dev/null
+++ b/templates/Bff/src/Infrastructure/packages.lock.json
@@ -0,0 +1,436 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "Microsoft.Extensions.Caching.Hybrid": {
+ "type": "Direct",
+ "requested": "[10.4.0, )",
+ "resolved": "10.4.0",
+ "contentHash": "4V+aMLQeU/p4VcIWIcvGro0L6HynmL2TrelL04Ce1iotP6T5+kjxuZQvl6P1ObSXIRPCbVXtQSt1NxK0fRIuag==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.4",
+ "Microsoft.Extensions.Caching.Memory": "10.0.4",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.4",
+ "Microsoft.Extensions.Options": "10.0.4"
+ }
+ },
+ "Microsoft.Extensions.Caching.StackExchangeRedis": {
+ "type": "Direct",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "zXb143/TpEKOLQuWGw2CkJgb9F4XXh2XbevMvppzsIHr1/pjML0zjc+vzXcpCV8YUwpW5NIaScZhzFSm621B3Q==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "StackExchange.Redis": "2.7.27"
+ }
+ },
+ "Microsoft.Extensions.Http.Polly": {
+ "type": "Direct",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "4VcH+2eVBQw3MtRpo02nrhv/nU54tL/pkcRF0fSSwD+8MoxgE1EjylPKbSIqHEK7iiB17I0iB37Ao8y+q1sV8g==",
+ "dependencies": {
+ "Microsoft.Extensions.Http": "10.0.5",
+ "Polly": "7.2.4",
+ "Polly.Extensions.Http": "3.0.0"
+ }
+ },
+ "OpenTelemetry.Exporter.Console": {
+ "type": "Direct",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "Jweov3Q70xmy5U8bwab8xd+xAuaFBI4695q/IpH4/dcAwKytyB+WhV5HufmKfXiKZhRbSEo8piG+i1ENEmdFXw==",
+ "dependencies": {
+ "OpenTelemetry": "1.15.0"
+ }
+ },
+ "OpenTelemetry.Exporter.OpenTelemetryProtocol": {
+ "type": "Direct",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "VH8ANc/js9IRvfYt0Q2UaAxNCOWm+IU+vWrtoH7pfx4oWPVdISUt+9uWfBCFMWZg5WzQip5dhslyDjeyZXXfSQ==",
+ "dependencies": {
+ "OpenTelemetry": "1.15.0"
+ }
+ },
+ "OpenTelemetry.Extensions": {
+ "type": "Direct",
+ "requested": "[1.14.0-beta.1, )",
+ "resolved": "1.14.0-beta.1",
+ "contentHash": "4DxYvBgz3OaCD8DE2lutiMiaEZh/PvWW05ewhAeqS6lKAETultAzfCveDXxMcIkaVwc93PR/Z22/P5zZ2rmHZA==",
+ "dependencies": {
+ "OpenTelemetry": "[1.14.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Extensions.Hosting": {
+ "type": "Direct",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "RixjKyB1pbYGhWdvPto4KJs+exdQknJsnjUO9WszdLles5Vcd0EYzxPNJdwmLjYfP+Jfbr4B5nktM4ZgeHSWtg==",
+ "dependencies": {
+ "Microsoft.Extensions.Hosting.Abstractions": "10.0.0",
+ "OpenTelemetry": "1.15.0"
+ }
+ },
+ "OpenTelemetry.Instrumentation.AspNetCore": {
+ "type": "Direct",
+ "requested": "[1.15.1, )",
+ "resolved": "1.15.1",
+ "contentHash": "wXaZTu6LHY8xcbRd6ClcrtjHqGVoGYCcArXEZA3iUjUcYSVYwDGyPU0PdkwTfylxv8JeCCVDQhVb0fT7xBJjGA==",
+ "dependencies": {
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.GrpcNetClient": {
+ "type": "Direct",
+ "requested": "[1.15.0-beta.1, )",
+ "resolved": "1.15.0-beta.1",
+ "contentHash": "SBas5+C4kGUqoy8OPpQis+QIgJ7/aaJl4H3oLzHCJnZLCb8TXZmQL2/r753RXXJUH8oIeLIzdW+EXgujSy+cpQ==",
+ "dependencies": {
+ "OpenTelemetry": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.Http": {
+ "type": "Direct",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "uToc7bUp8IEdb0ny9mKsL6FrrYelINPzxxiSShJgOf4XmQc4Azww6S5RjRj24YhsOn2a1MABOrxfVTZXtDk4Eg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.0",
+ "Microsoft.Extensions.Options": "10.0.0",
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.Process": {
+ "type": "Direct",
+ "requested": "[1.15.0-beta.1, )",
+ "resolved": "1.15.0-beta.1",
+ "contentHash": "99zSvpwsMpKextd3RkHGY8iRsmw1qg3PjqkjC5hZI0fZG6m+wPsssrX6z9RhwDGcZ8sdGNjoKyLpNOmR154zQg==",
+ "dependencies": {
+ "OpenTelemetry.Api": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.Runtime": {
+ "type": "Direct",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "OOvpqR/j2Pb6+tWhHNODIbSJ53Or/MDtTiXEyrsWI02K2lLAgvBFcxUOrHggS/8015cYR3AdSaXv6NZrkz5yQA==",
+ "dependencies": {
+ "OpenTelemetry.Api": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.StackExchangeRedis": {
+ "type": "Direct",
+ "requested": "[1.15.0-beta.1, )",
+ "resolved": "1.15.0-beta.1",
+ "contentHash": "Igg/3MlBZZ9lZCTzMcvoFKav263+zOcKx9s4LVIdq96YmBHCuPmDiyygAIPdeIVzwN08VwD3RG1nXHDuRF1Ssg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.0",
+ "Microsoft.Extensions.Options": "10.0.0",
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)",
+ "StackExchange.Redis": "2.6.122"
+ }
+ },
+ "Google.Protobuf": {
+ "type": "Transitive",
+ "resolved": "3.31.1",
+ "contentHash": "gSnJbUmGiOTdWddPhqzrEscHq9Ls6sqRDPB9WptckyjTUyx70JOOAaDLkFff8gManZNN3hllQ4aQInnQyq/Z/A=="
+ },
+ "Grpc.AspNetCore.Server": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "diSC/ZeNdSdxHdYSOpYwuSBBDYpuNVtJQFJfiBB0WrYOQ4lVMmdxuUZJcViahQyo8pCvS3Mueo5lqFxwwMF/iw==",
+ "dependencies": {
+ "Grpc.Net.Common": "2.76.0"
+ }
+ },
+ "Grpc.AspNetCore.Server.ClientFactory": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "y5KGO1GO0N2L/hCCMR05mmoK8j+v8rKvZ+9nothAxKx2Tf2CwV8f4TM5K0GkKfDsp4vrc4lm90MU6E+DeN7YIw==",
+ "dependencies": {
+ "Grpc.AspNetCore.Server": "2.76.0",
+ "Grpc.Net.ClientFactory": "2.76.0"
+ }
+ },
+ "Grpc.Core.Api": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "cSxC2tdnFdXXuBgIn1pjc4YBx7LXTCp4M0qn+SMBS35VWZY+cEQYLWTBDDhdBH1HzU7BV+ncVZlniGQHMpRJKQ=="
+ },
+ "Grpc.Net.Client": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "K1oldmqw2+Gn69nGRzZLhqSiUZwelX1GrBu/cUl9wNf1C0uB61vFS6JcxUUv9P8VoUJhFsmV44JA6lI2EUt4xw==",
+ "dependencies": {
+ "Grpc.Net.Common": "2.76.0",
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.0"
+ }
+ },
+ "Grpc.Net.ClientFactory": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "XI+kO69L9AV8B9N0UQOmH911r6MOEp9huHiavEsY56DJYuzJ9KAxNGy37dpV6CLbgCaN2uKmpOsZ9Pao6bmpVQ==",
+ "dependencies": {
+ "Grpc.Net.Client": "2.76.0",
+ "Microsoft.Extensions.Http": "8.0.0"
+ }
+ },
+ "Grpc.Net.Common": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "bZpiMVYgvpB44/wBh1RotrkqC7bg2FOasLri2GhR3hMKyzsiTxCoDE49YjPrJeFc4RW0wS8u+EInI09sjxVFRA==",
+ "dependencies": {
+ "Grpc.Core.Api": "2.76.0"
+ }
+ },
+ "Grpc.Tools": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "goRzYZVMgQtyLvkWdgTnPycg49hlNxnMkqGGgR3l7nCOm0bUh0YeAneiJ9JFk3XLgF4suQUdETYkl2Mg/TBr0w=="
+ },
+ "Microsoft.Extensions.Caching.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Caching.Memory": {
+ "type": "Transitive",
+ "resolved": "10.0.4",
+ "contentHash": "CLLussNUMdSbyJOu4VBF7sqskHGB/5N1EcFzrqG/HsPATN8fCRUcfp0qns1VwkxKHwxrtYCh5FKe+kM81Q1PHA==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.4",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.4",
+ "Microsoft.Extensions.Options": "10.0.4",
+ "Microsoft.Extensions.Primitives": "10.0.4"
+ }
+ },
+ "Microsoft.Extensions.Configuration": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA=="
+ },
+ "Microsoft.Extensions.Diagnostics": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "vAJHd4yOpmKoK+jBuYV7a3y+Ab9U4ARCc29b6qvMy276RgJFw9LFs0DdsPqOL3ahwzyrX7tM+i4cCxU/RX0qAg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Diagnostics.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.FileProviders.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "/ppSdehKk3fuXjlqCDgSOtjRK/pSHU8eWgzSHfHdwVm5BP4Dgejehkw+PtxKG2j98qTDEHDst2Y99aNsmJldmw==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.0"
+ }
+ },
+ "Microsoft.Extensions.Hosting.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "KrN6TGFwCwqOkLLk/idW/XtDQh+8In+CL9T4M1Dx+5ScsjTq4TlVbal8q532m82UYrMr6RiQJF2HvYCN0QwVsA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.0",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.0"
+ }
+ },
+ "Microsoft.Extensions.Http": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "AiFvHYM8nP0wPC7bGPI3NHQlSYSLqjjT7DMJUuuxhd+7pz3O89iu2gdQfgACy5DxsXENiok5i1bMacJL7KR8jA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Diagnostics": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Logging.Configuration": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "j8zcwhS6bYB6FEfaY3nYSgHdpiL2T+/V3xjpHtslVAegyI1JUbB9yAt/BFdvZdsNbY0Udm4xFtvfT/hUwcOOOg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.0",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Configuration.Binder": "10.0.0",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Logging": "10.0.0",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Options": "10.0.0",
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.0"
+ }
+ },
+ "Microsoft.Extensions.Options": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Options.ConfigurationExtensions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Configuration.Binder": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g=="
+ },
+ "OpenTelemetry": {
+ "type": "Transitive",
+ "resolved": "1.15.0",
+ "contentHash": "7mS/oZFF8S6xyqGQfMU1btp0nXJQUPWV535Vp/XMLYwRAUv36xQN+U4vufWBF1+z4HnRTOwuFHtUSGnHbyN6FQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Logging.Configuration": "10.0.0",
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.0"
+ }
+ },
+ "OpenTelemetry.Api": {
+ "type": "Transitive",
+ "resolved": "1.15.0",
+ "contentHash": "vk5OGdf6K9kQScCWo3bRjhDWCv6Pqw92IpX4dlARZ8B1WL7/2NGTDtCkkw42eQf7UdwyoHKzVvMH/PtL8d6z7w=="
+ },
+ "OpenTelemetry.Api.ProviderBuilderExtensions": {
+ "type": "Transitive",
+ "resolved": "1.15.0",
+ "contentHash": "OnuSUlRpGvowkOzGFQfy+KZFu0cITfKfh2IYJJiZskxVJiOuexwOOuvfDAgpJdmTzVWAHjYdz2shcHZaJ06UjQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0",
+ "OpenTelemetry.Api": "1.15.0"
+ }
+ },
+ "Pipelines.Sockets.Unofficial": {
+ "type": "Transitive",
+ "resolved": "2.2.8",
+ "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ=="
+ },
+ "Polly": {
+ "type": "Transitive",
+ "resolved": "7.2.4",
+ "contentHash": "bw00Ck5sh6ekduDE3mnCo1ohzuad946uslCDEENu3091+6UKnBuKLo4e+yaNcCzXxOZCXWY2gV4a35+K1d4LDA=="
+ },
+ "Polly.Extensions.Http": {
+ "type": "Transitive",
+ "resolved": "3.0.0",
+ "contentHash": "drrG+hB3pYFY7w1c3BD+lSGYvH2oIclH8GRSehgfyP5kjnFnHKQuuBhuHLv+PWyFuaTDyk/vfRpnxOzd11+J8g==",
+ "dependencies": {
+ "Polly": "7.1.0"
+ }
+ },
+ "StackExchange.Redis": {
+ "type": "Transitive",
+ "resolved": "2.7.27",
+ "contentHash": "Uqc2OQHglqj9/FfGQ6RkKFkZfHySfZlfmbCl+hc+u2I/IqunfelQ7QJi7ZhvAJxUtu80pildVX6NPLdDaUffOw==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "6.0.0",
+ "Pipelines.Sockets.Unofficial": "2.2.8"
+ }
+ },
+ "contracts": {
+ "type": "Project",
+ "dependencies": {
+ "Grpc.AspNetCore": "[2.76.0, )"
+ }
+ },
+ "Grpc.AspNetCore": {
+ "type": "CentralTransitive",
+ "requested": "[2.76.0, )",
+ "resolved": "2.76.0",
+ "contentHash": "LyXMmpN2Ba0TE35SOLSKbGqIYtJuhc1UgiaGfoW1X8KJERV70QI5KGW+ckEY7MrXoFWN/uWo4B70siVhbDmCgQ==",
+ "dependencies": {
+ "Google.Protobuf": "3.31.1",
+ "Grpc.AspNetCore.Server.ClientFactory": "2.76.0",
+ "Grpc.Tools": "2.76.0"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Logging": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/templates/Bff/src/MockApi/Endpoints/EndpointExtensions.cs b/templates/Bff/src/MockApi/Endpoints/EndpointExtensions.cs
new file mode 100644
index 00000000..5a4165a1
--- /dev/null
+++ b/templates/Bff/src/MockApi/Endpoints/EndpointExtensions.cs
@@ -0,0 +1,10 @@
+namespace MockApi.Endpoints;
+
+internal static class EndpointExtensions
+{
+ public static WebApplication MapEndpoints(this WebApplication app)
+ {
+ app.MapOrderEndpoints();
+ return app;
+ }
+}
\ No newline at end of file
diff --git a/templates/Bff/src/MockApi/Endpoints/OrderEndpoints.cs b/templates/Bff/src/MockApi/Endpoints/OrderEndpoints.cs
new file mode 100644
index 00000000..59aa2174
--- /dev/null
+++ b/templates/Bff/src/MockApi/Endpoints/OrderEndpoints.cs
@@ -0,0 +1,32 @@
+using AutoFixture;
+using Contracts.Common;
+using Contracts.Orders;
+using Microsoft.AspNetCore.Mvc;
+
+namespace MockApi.Endpoints;
+
+internal static class OrderEndpoints
+{
+ public static WebApplication MapOrderEndpoints(this WebApplication app)
+ {
+ var ordersGroup = app.MapGroup("orders").WithTags("orders");
+ var autoFixture = new Fixture();
+
+ ordersGroup.MapGet("/{id}", async (
+ [FromRoute] int id,
+ CancellationToken cancellationToken
+ ) => id switch
+ {
+ 1 => Results.Ok(autoFixture.Create>()),
+ 2 => Results.Ok(autoFixture.Create>()),
+ _ => Results.NotFound(autoFixture.Create())
+ });
+
+ ordersGroup.MapPost("/", async (
+ [FromBody] CreateOrderRequest request,
+ CancellationToken cancellationToken
+ ) => Results.Created($"/orders/1", autoFixture.Create>()));
+
+ return app;
+ }
+}
diff --git a/templates/Bff/src/MockApi/GrpcServices/GrpcServiceExtensions.cs b/templates/Bff/src/MockApi/GrpcServices/GrpcServiceExtensions.cs
new file mode 100644
index 00000000..a4c0d044
--- /dev/null
+++ b/templates/Bff/src/MockApi/GrpcServices/GrpcServiceExtensions.cs
@@ -0,0 +1,11 @@
+namespace MockApi.GrpcServices;
+
+internal static class GrpcServiceExtensions
+{
+ public static WebApplication MapGrpcServices(this WebApplication app)
+ {
+ app.MapGrpcService();
+
+ return app;
+ }
+}
\ No newline at end of file
diff --git a/templates/Bff/src/MockApi/GrpcServices/PaymentService.cs b/templates/Bff/src/MockApi/GrpcServices/PaymentService.cs
new file mode 100644
index 00000000..bfec4737
--- /dev/null
+++ b/templates/Bff/src/MockApi/GrpcServices/PaymentService.cs
@@ -0,0 +1,14 @@
+using Grpc.Core;
+using GrpcPayment;
+using static GrpcPayment.PaymentService;
+
+namespace MockApi.GrpcServices;
+
+public class PaymentService : PaymentServiceBase
+{
+ public override async Task Create(CreatePaymentRequest request, ServerCallContext context) => new()
+ {
+ Success = true,
+ Message = "Payment created successfully"
+ };
+}
diff --git a/templates/Bff/src/MockApi/MockApi.csproj b/templates/Bff/src/MockApi/MockApi.csproj
new file mode 100644
index 00000000..95f8b3af
--- /dev/null
+++ b/templates/Bff/src/MockApi/MockApi.csproj
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/Bff/src/MockApi/MockApi.http b/templates/Bff/src/MockApi/MockApi.http
new file mode 100644
index 00000000..a61bd2b1
--- /dev/null
+++ b/templates/Bff/src/MockApi/MockApi.http
@@ -0,0 +1,44 @@
+@WebApp_HostAddress = https://localhost:7177
+
+###
+GET {{WebApp_HostAddress}}/orders/1 HTTP/2
+CorrelationId: {{$guid}}
+CacheEnabled: false
+Accept-Encoding: gzip, deflate
+Accept: application/json
+
+###
+GET {{WebApp_HostAddress}}/orders/2 HTTP/2
+CorrelationId: {{$guid}}
+CacheEnabled: false
+Accept-Encoding: gzip, deflate
+Accept: application/json
+
+###
+GET {{WebApp_HostAddress}}/orders/4 HTTP/2
+CorrelationId: {{$guid}}
+CacheEnabled: false
+Accept-Encoding: gzip, deflate
+Accept: application/json
+
+
+###
+POST {{WebApp_HostAddress}}/orders HTTP/2
+Content-Type: application/json
+
+{
+ "CorrelationId": "{{$guid}}",
+ "Description": "John's computer",
+ "Items": [
+ {
+ "Name": "Computer",
+ "Description": "Surface 2",
+ "Value": 1000
+ },
+ {
+ "Name": "Mouse",
+ "Description": "Microsoft mouse",
+ "Value": 99
+ }
+ ]
+}
diff --git a/templates/Bff/src/MockApi/Program.cs b/templates/Bff/src/MockApi/Program.cs
new file mode 100644
index 00000000..a27f368f
--- /dev/null
+++ b/templates/Bff/src/MockApi/Program.cs
@@ -0,0 +1,86 @@
+using HealthChecks.UI.Client;
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using Microsoft.AspNetCore.ResponseCompression;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using MockApi.Endpoints;
+using MockApi.GrpcServices;
+using OpenTelemetry.Logs;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddGrpc();
+builder.Services.AddResponseCompression(
+ options =>
+ {
+ options.EnableForHttps = true;
+ options.MimeTypes = ResponseCompressionDefaults.MimeTypes;
+ options.Providers.Add();
+ }
+);
+builder.Services
+ .AddHealthChecks()
+ .AddCheck("self", () => HealthCheckResult.Healthy());
+
+builder.WebHost.ConfigureKestrel(options => options
+ .ConfigureEndpointDefaults(listenOptions => listenOptions.Protocols = HttpProtocols.Http2
+));
+
+var serviceName = "Hexagonal.Solution.Template.Bff.MockApi";
+var serviceVersion = typeof(Program).Assembly.GetName().Version!.ToString();
+var resourceBuilder = ResourceBuilder
+ .CreateDefault()
+ .AddService(serviceName, serviceVersion: serviceVersion);
+
+builder.Services.AddOpenTelemetry()
+ .WithMetrics(metrics => metrics
+ .SetResourceBuilder(resourceBuilder)
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation()
+ .AddProcessInstrumentation()
+ .AddOtlpExporter()
+ )
+ .WithTracing(tracing => tracing
+ .AddSource(serviceName)
+ .SetResourceBuilder(resourceBuilder)
+ .AddGrpcClientInstrumentation()
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddOtlpExporter()
+ );
+
+builder.Logging.AddOpenTelemetry(options =>
+{
+ options.IncludeFormattedMessage = true;
+ options.IncludeScopes = true;
+ options.ParseStateValues = true;
+ options
+ .SetResourceBuilder(resourceBuilder)
+ .AttachLogsToActivityEvent()
+ .AddOtlpExporter();
+});
+
+var app = builder.Build();
+
+app.UseHttpsRedirection();
+
+app.MapEndpoints()
+ .MapGrpcServices()
+ .UseResponseCompression();
+
+app.UseHealthChecks("/health", new HealthCheckOptions
+{
+ Predicate = _ => true,
+ ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
+});
+
+app.UseHealthChecks("/live", new HealthCheckOptions
+{
+ Predicate = r => r.Name.Contains("self")
+});
+
+await app.RunAsync();
diff --git a/templates/Bff/src/MockApi/Properties/launchSettings.json b/templates/Bff/src/MockApi/Properties/launchSettings.json
new file mode 100644
index 00000000..483c40c3
--- /dev/null
+++ b/templates/Bff/src/MockApi/Properties/launchSettings.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "Development": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "https://*:7177;http://*:5012",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "OTEL_SERVICE_NAME": "Hexagonal.Solution.Template.Bff.MockApi",
+ "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317",
+ "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
+ "OTEL_RESOURCE_ATTRIBUTES": "service.namespace=development",
+ }
+ }
+ }
+}
diff --git a/templates/Bff/src/MockApi/packages.lock.json b/templates/Bff/src/MockApi/packages.lock.json
new file mode 100644
index 00000000..f336c9ba
--- /dev/null
+++ b/templates/Bff/src/MockApi/packages.lock.json
@@ -0,0 +1,226 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "AspNetCore.HealthChecks.UI.Client": {
+ "type": "Direct",
+ "requested": "[9.0.0, )",
+ "resolved": "9.0.0",
+ "contentHash": "1Ub3Wvvbz7CMuFNWgLEc9qqQibiMoovDML/WHrwr5J83RPgtI20giCR92s/ipLgu7IIuqw+W/y7WpIeHqAICxg==",
+ "dependencies": {
+ "AspNetCore.HealthChecks.UI.Core": "9.0.0"
+ }
+ },
+ "AutoFixture": {
+ "type": "Direct",
+ "requested": "[4.18.1, )",
+ "resolved": "4.18.1",
+ "contentHash": "BmWZDY4fkrYOyd5/CTBOeXbzsNwV8kI4kDi/Ty1Y5F+WDHBVKxzfWlBE4RSicvZ+EOi2XDaN5uwdrHsItLW6Kw==",
+ "dependencies": {
+ "Fare": "[2.1.1, 3.0.0)"
+ }
+ },
+ "OpenTelemetry.Exporter.Console": {
+ "type": "Direct",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "Jweov3Q70xmy5U8bwab8xd+xAuaFBI4695q/IpH4/dcAwKytyB+WhV5HufmKfXiKZhRbSEo8piG+i1ENEmdFXw==",
+ "dependencies": {
+ "OpenTelemetry": "1.15.0"
+ }
+ },
+ "OpenTelemetry.Exporter.OpenTelemetryProtocol": {
+ "type": "Direct",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "VH8ANc/js9IRvfYt0Q2UaAxNCOWm+IU+vWrtoH7pfx4oWPVdISUt+9uWfBCFMWZg5WzQip5dhslyDjeyZXXfSQ==",
+ "dependencies": {
+ "OpenTelemetry": "1.15.0"
+ }
+ },
+ "OpenTelemetry.Extensions": {
+ "type": "Direct",
+ "requested": "[1.14.0-beta.1, )",
+ "resolved": "1.14.0-beta.1",
+ "contentHash": "4DxYvBgz3OaCD8DE2lutiMiaEZh/PvWW05ewhAeqS6lKAETultAzfCveDXxMcIkaVwc93PR/Z22/P5zZ2rmHZA==",
+ "dependencies": {
+ "OpenTelemetry": "[1.14.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Extensions.Hosting": {
+ "type": "Direct",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "RixjKyB1pbYGhWdvPto4KJs+exdQknJsnjUO9WszdLles5Vcd0EYzxPNJdwmLjYfP+Jfbr4B5nktM4ZgeHSWtg==",
+ "dependencies": {
+ "OpenTelemetry": "1.15.0"
+ }
+ },
+ "OpenTelemetry.Instrumentation.AspNetCore": {
+ "type": "Direct",
+ "requested": "[1.15.1, )",
+ "resolved": "1.15.1",
+ "contentHash": "wXaZTu6LHY8xcbRd6ClcrtjHqGVoGYCcArXEZA3iUjUcYSVYwDGyPU0PdkwTfylxv8JeCCVDQhVb0fT7xBJjGA==",
+ "dependencies": {
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.GrpcNetClient": {
+ "type": "Direct",
+ "requested": "[1.15.0-beta.1, )",
+ "resolved": "1.15.0-beta.1",
+ "contentHash": "SBas5+C4kGUqoy8OPpQis+QIgJ7/aaJl4H3oLzHCJnZLCb8TXZmQL2/r753RXXJUH8oIeLIzdW+EXgujSy+cpQ==",
+ "dependencies": {
+ "OpenTelemetry": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.Http": {
+ "type": "Direct",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "uToc7bUp8IEdb0ny9mKsL6FrrYelINPzxxiSShJgOf4XmQc4Azww6S5RjRj24YhsOn2a1MABOrxfVTZXtDk4Eg==",
+ "dependencies": {
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.Process": {
+ "type": "Direct",
+ "requested": "[1.15.0-beta.1, )",
+ "resolved": "1.15.0-beta.1",
+ "contentHash": "99zSvpwsMpKextd3RkHGY8iRsmw1qg3PjqkjC5hZI0fZG6m+wPsssrX6z9RhwDGcZ8sdGNjoKyLpNOmR154zQg==",
+ "dependencies": {
+ "OpenTelemetry.Api": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.Runtime": {
+ "type": "Direct",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "OOvpqR/j2Pb6+tWhHNODIbSJ53Or/MDtTiXEyrsWI02K2lLAgvBFcxUOrHggS/8015cYR3AdSaXv6NZrkz5yQA==",
+ "dependencies": {
+ "OpenTelemetry.Api": "[1.15.0, 2.0.0)"
+ }
+ },
+ "AspNetCore.HealthChecks.UI.Core": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "TVriy4hgYnhfqz6NAzv8qe62Q8wf82iKUL6WV9selqeFZTq1ILi39Sic6sFQegRysvAVcnxKP/vY8z9Fk8x6XQ=="
+ },
+ "Fare": {
+ "type": "Transitive",
+ "resolved": "2.1.1",
+ "contentHash": "HaI8puqA66YU7/9cK4Sgbs1taUTP1Ssa4QT2PIzqJ7GvAbN1QgkjbRsjH+FSbMh1MJdvS0CIwQNLtFT+KF6KpA==",
+ "dependencies": {
+ "NETStandard.Library": "1.6.1"
+ }
+ },
+ "Google.Protobuf": {
+ "type": "Transitive",
+ "resolved": "3.31.1",
+ "contentHash": "gSnJbUmGiOTdWddPhqzrEscHq9Ls6sqRDPB9WptckyjTUyx70JOOAaDLkFff8gManZNN3hllQ4aQInnQyq/Z/A=="
+ },
+ "Grpc.AspNetCore.Server": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "diSC/ZeNdSdxHdYSOpYwuSBBDYpuNVtJQFJfiBB0WrYOQ4lVMmdxuUZJcViahQyo8pCvS3Mueo5lqFxwwMF/iw==",
+ "dependencies": {
+ "Grpc.Net.Common": "2.76.0"
+ }
+ },
+ "Grpc.AspNetCore.Server.ClientFactory": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "y5KGO1GO0N2L/hCCMR05mmoK8j+v8rKvZ+9nothAxKx2Tf2CwV8f4TM5K0GkKfDsp4vrc4lm90MU6E+DeN7YIw==",
+ "dependencies": {
+ "Grpc.AspNetCore.Server": "2.76.0",
+ "Grpc.Net.ClientFactory": "2.76.0"
+ }
+ },
+ "Grpc.Core.Api": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "cSxC2tdnFdXXuBgIn1pjc4YBx7LXTCp4M0qn+SMBS35VWZY+cEQYLWTBDDhdBH1HzU7BV+ncVZlniGQHMpRJKQ=="
+ },
+ "Grpc.Net.Client": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "K1oldmqw2+Gn69nGRzZLhqSiUZwelX1GrBu/cUl9wNf1C0uB61vFS6JcxUUv9P8VoUJhFsmV44JA6lI2EUt4xw==",
+ "dependencies": {
+ "Grpc.Net.Common": "2.76.0"
+ }
+ },
+ "Grpc.Net.ClientFactory": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "XI+kO69L9AV8B9N0UQOmH911r6MOEp9huHiavEsY56DJYuzJ9KAxNGy37dpV6CLbgCaN2uKmpOsZ9Pao6bmpVQ==",
+ "dependencies": {
+ "Grpc.Net.Client": "2.76.0"
+ }
+ },
+ "Grpc.Net.Common": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "bZpiMVYgvpB44/wBh1RotrkqC7bg2FOasLri2GhR3hMKyzsiTxCoDE49YjPrJeFc4RW0wS8u+EInI09sjxVFRA==",
+ "dependencies": {
+ "Grpc.Core.Api": "2.76.0"
+ }
+ },
+ "Grpc.Tools": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "goRzYZVMgQtyLvkWdgTnPycg49hlNxnMkqGGgR3l7nCOm0bUh0YeAneiJ9JFk3XLgF4suQUdETYkl2Mg/TBr0w=="
+ },
+ "Microsoft.NETCore.Platforms": {
+ "type": "Transitive",
+ "resolved": "1.1.0",
+ "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
+ },
+ "NETStandard.Library": {
+ "type": "Transitive",
+ "resolved": "1.6.1",
+ "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0"
+ }
+ },
+ "OpenTelemetry": {
+ "type": "Transitive",
+ "resolved": "1.15.0",
+ "contentHash": "7mS/oZFF8S6xyqGQfMU1btp0nXJQUPWV535Vp/XMLYwRAUv36xQN+U4vufWBF1+z4HnRTOwuFHtUSGnHbyN6FQ==",
+ "dependencies": {
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.0"
+ }
+ },
+ "OpenTelemetry.Api": {
+ "type": "Transitive",
+ "resolved": "1.15.0",
+ "contentHash": "vk5OGdf6K9kQScCWo3bRjhDWCv6Pqw92IpX4dlARZ8B1WL7/2NGTDtCkkw42eQf7UdwyoHKzVvMH/PtL8d6z7w=="
+ },
+ "OpenTelemetry.Api.ProviderBuilderExtensions": {
+ "type": "Transitive",
+ "resolved": "1.15.0",
+ "contentHash": "OnuSUlRpGvowkOzGFQfy+KZFu0cITfKfh2IYJJiZskxVJiOuexwOOuvfDAgpJdmTzVWAHjYdz2shcHZaJ06UjQ==",
+ "dependencies": {
+ "OpenTelemetry.Api": "1.15.0"
+ }
+ },
+ "contracts": {
+ "type": "Project",
+ "dependencies": {
+ "Grpc.AspNetCore": "[2.76.0, )"
+ }
+ },
+ "Grpc.AspNetCore": {
+ "type": "CentralTransitive",
+ "requested": "[2.76.0, )",
+ "resolved": "2.76.0",
+ "contentHash": "LyXMmpN2Ba0TE35SOLSKbGqIYtJuhc1UgiaGfoW1X8KJERV70QI5KGW+ckEY7MrXoFWN/uWo4B70siVhbDmCgQ==",
+ "dependencies": {
+ "Google.Protobuf": "3.31.1",
+ "Grpc.AspNetCore.Server.ClientFactory": "2.76.0",
+ "Grpc.Tools": "2.76.0"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/templates/Bff/src/WebApp/Endpoints/EndpointExtensions.cs b/templates/Bff/src/WebApp/Endpoints/EndpointExtensions.cs
new file mode 100644
index 00000000..64c8b12a
--- /dev/null
+++ b/templates/Bff/src/WebApp/Endpoints/EndpointExtensions.cs
@@ -0,0 +1,13 @@
+namespace WebApp.Endpoints;
+
+internal static class EndpointExtensions
+{
+ public static WebApplication MapEndpoints(this WebApplication app)
+ {
+ app
+ .MapOrderEndpoints()
+ .MapPaymentEndpoints();
+
+ return app;
+ }
+}
\ No newline at end of file
diff --git a/templates/Bff/src/WebApp/Endpoints/OrderEndpoints.cs b/templates/Bff/src/WebApp/Endpoints/OrderEndpoints.cs
new file mode 100644
index 00000000..36a59baf
--- /dev/null
+++ b/templates/Bff/src/WebApp/Endpoints/OrderEndpoints.cs
@@ -0,0 +1,80 @@
+using Infrastructure.Http;
+using Microsoft.AspNetCore.Mvc;
+using Infrastructure.Cache;
+using Contracts.Orders;
+using Contracts.Common;
+using System.Globalization;
+using Infrastructure.Common;
+
+namespace WebApp.Endpoints;
+
+internal static class OrderEndpoints
+{
+ public static WebApplication MapOrderEndpoints(this WebApplication app)
+ {
+ var serviceKey = ServicesKey.Orders.ToString();
+
+ var ordersGroup = app.MapGroup(serviceKey)
+ .WithTags(serviceKey)
+ .RequireRateLimiting(serviceKey);
+
+ ordersGroup.MapGet("/{id}", async (
+ [FromKeyedServices(ServicesKey.Orders)] BaseHttpService httpService,
+ [FromRoute] int id,
+ [FromServices] HybridCacheService cache,
+ CancellationToken cancellationToken,
+ [FromHeader] Guid? correlationId = null,
+ [FromHeader] bool cacheEnabled = true
+ ) => {
+ var response = cacheEnabled switch
+ {
+ true => await cache.GetOrCreateAsync(
+ $"{nameof(OrderEndpoints)}-{id}",
+ async (cancellationToken) => await httpService.SendAsync>($"/orders/{id}", HttpMethod.Get, cancellationToken, headers: new()
+ {
+ { "CorrelationId", (correlationId ?? Guid.NewGuid()).ToString() }
+ }),
+ cancellationToken
+ ),
+ false or _ => await httpService.SendAsync>($"/orders/{id}", HttpMethod.Get, cancellationToken, headers: new()
+ {
+ { "CorrelationId", (correlationId ?? Guid.NewGuid()).ToString() }
+ }),
+ };
+
+ return response != null ? Results.Ok(response) : Results.NotFound(response);
+ })
+ .Produces>(StatusCodes.Status200OK)
+ .Produces(StatusCodes.Status404NotFound)
+ .Produces(StatusCodes.Status400BadRequest)
+ .Produces(StatusCodes.Status500InternalServerError)
+ .WithDescription("Gets an order by its identifier")
+ .WithName("GetById");
+
+ ordersGroup.MapPost("/", async (
+ [FromBody] CreateOrderRequest request,
+ [FromKeyedServices(ServicesKey.Orders)] BaseHttpService httpService,
+ CancellationToken cancellationToken
+ ) =>
+ {
+ var response = await httpService.SendAsync>(
+ "orders", HttpMethod.Post, request,
+ cancellationToken: cancellationToken
+ );
+
+ if (response == null)
+ return Results.BadRequest();
+
+ var id = response.Data?.Id.ToString(CultureInfo.InvariantCulture) ?? "unknown";
+
+ return Results.Created($"/orders/{id}", response);
+ })
+ .Produces>(StatusCodes.Status201Created)
+ .Produces(StatusCodes.Status400BadRequest)
+ .Produces(StatusCodes.Status500InternalServerError)
+ .WithDescription("Creates a new order")
+ .WithName("Create");
+
+ return app;
+ }
+}
diff --git a/templates/Bff/src/WebApp/Endpoints/PaymentEndpoints.cs b/templates/Bff/src/WebApp/Endpoints/PaymentEndpoints.cs
new file mode 100644
index 00000000..4c43276f
--- /dev/null
+++ b/templates/Bff/src/WebApp/Endpoints/PaymentEndpoints.cs
@@ -0,0 +1,36 @@
+using Contracts.Common;
+using GrpcPayment;
+using Infrastructure.Common;
+using Infrastructure.Grpc;
+using Microsoft.AspNetCore.Mvc;
+
+namespace WebApp.Endpoints;
+
+public static partial class PaymentEndpoints
+{
+ public static IEndpointRouteBuilder MapPaymentEndpoints(this IEndpointRouteBuilder endpoints)
+ {
+ var serviceKey = ServicesKey.Payments.ToString();
+
+ var group = endpoints.MapGroup(serviceKey)
+ .WithTags(serviceKey)
+ .RequireRateLimiting(serviceKey);
+
+ group.MapPost("/", async (
+ [FromServices] PaymentsService paymentsService,
+ [FromBody] CreatePaymentRequest request
+ ) =>
+ {
+ var result = await paymentsService.CreatePaymentAsync(request);
+
+ return result.Success ? Results.Ok(result) : Results.BadRequest(result);
+ })
+ .Produces(StatusCodes.Status200OK)
+ .Produces(StatusCodes.Status400BadRequest)
+ .Produces(StatusCodes.Status500InternalServerError)
+ .WithDescription("Creates a new payment")
+ .WithName("CreatePayment");
+
+ return endpoints;
+ }
+}
diff --git a/templates/Bff/src/WebApp/Extensions/HealthCheckExtensions.cs b/templates/Bff/src/WebApp/Extensions/HealthCheckExtensions.cs
new file mode 100644
index 00000000..b8c36ea2
--- /dev/null
+++ b/templates/Bff/src/WebApp/Extensions/HealthCheckExtensions.cs
@@ -0,0 +1,66 @@
+using HealthChecks.UI.Client;
+using Infrastructure.Common;
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace WebApp.Extensions;
+
+internal static class HealthCheckExtensions
+{
+ public static IServiceCollection AddCustomHealthChecks(
+ this IServiceCollection services,
+ IConfiguration configuration
+ )
+ {
+ var healthChecksBuilder = services
+ .AddHealthChecks()
+ .AddCheck("self", () => HealthCheckResult.Healthy())
+ .AddRedis(
+ configuration.GetConnectionString("Redis")!,
+ name: "Redis",
+ tags: ["services"]
+ );
+
+ var serviceKeys = Enum.GetValues();
+ foreach (var serviceKey in serviceKeys)
+ {
+ var baseAddress = configuration.GetSection("Http")
+ .GetChildren()
+ .FirstOrDefault(x => x["Name"] == serviceKey.ToString())?["BaseAddress"];
+
+ if (!string.IsNullOrEmpty(baseAddress))
+ {
+ healthChecksBuilder.AddUrlGroup(
+ new Uri($"{baseAddress}/health"),
+ name: serviceKey.ToString(),
+ tags: ["services"]
+ );
+ }
+ }
+
+ return services;
+ }
+
+ public static IApplicationBuilder UseCustomHealthChecks(
+ this IApplicationBuilder app
+ )
+ {
+ app.UseHealthChecks("/health", new HealthCheckOptions()
+ {
+ Predicate = _ => true,
+ ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
+ });
+
+ app.UseHealthChecks("/live", new HealthCheckOptions
+ {
+ Predicate = r => r.Name.Contains("self")
+ });
+
+ app.UseHealthChecks("/ready", new HealthCheckOptions
+ {
+ Predicate = r => r.Tags.Contains("services")
+ });
+
+ return app;
+ }
+}
\ No newline at end of file
diff --git a/templates/Bff/src/WebApp/Extensions/RateLimitExtensions.cs b/templates/Bff/src/WebApp/Extensions/RateLimitExtensions.cs
new file mode 100644
index 00000000..5eb1d745
--- /dev/null
+++ b/templates/Bff/src/WebApp/Extensions/RateLimitExtensions.cs
@@ -0,0 +1,41 @@
+using Infrastructure.Common;
+using Microsoft.AspNetCore.RateLimiting;
+
+namespace WebApp.Extensions;
+
+internal static class RateLimitExtensions
+{
+ extension(IServiceCollection services)
+ {
+ internal IServiceCollection AddRateLimiting(IConfiguration configuration)
+ {
+ if (!configuration.GetValue("RATE_LIMITING_ENABLED"))
+ return services;
+
+ var serviceKeys = Enum.GetValues();
+ foreach (var serviceKey in serviceKeys)
+ {
+ var serviceConfig = configuration.GetSection("Services")
+ .GetChildren()
+ .FirstOrDefault(x => x["Name"] == serviceKey.ToString());
+
+ if (serviceConfig != null && int.TryParse(serviceConfig["LimitPerMinute"], out int limitPerMinute))
+ {
+ services.AddRateLimiter(options =>
+ {
+ options.AddFixedWindowLimiter(serviceKey.ToString(), limiterOptions =>
+ {
+ limiterOptions.PermitLimit = limitPerMinute;
+ limiterOptions.Window = TimeSpan.FromMinutes(1);
+ });
+
+ options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
+ });
+ }
+ }
+
+ return services;
+ }
+ }
+}
+
\ No newline at end of file
diff --git a/templates/Bff/src/WebApp/Middlewares/ExceptionHandlingMiddleware.cs b/templates/Bff/src/WebApp/Middlewares/ExceptionHandlingMiddleware.cs
new file mode 100644
index 00000000..4970e848
--- /dev/null
+++ b/templates/Bff/src/WebApp/Middlewares/ExceptionHandlingMiddleware.cs
@@ -0,0 +1,32 @@
+using System.Net;
+using Infrastructure.Common;
+
+namespace WebApp.Middlewares;
+
+internal sealed class ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger)
+{
+ private readonly RequestDelegate _next = next;
+ private readonly ILogger _logger = logger;
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ try
+ {
+ await _next(context);
+ }
+ catch (Exception ex)
+ {
+ await HandleExceptionAsync(context, ex);
+ }
+ }
+
+ private async Task HandleExceptionAsync(HttpContext context, Exception exception)
+ {
+ context.Response.ContentType = "application/json";
+ context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
+
+ Logs.Error(_logger, exception.Message);
+ }
+}
+
+public record ExceptionResponse(HttpStatusCode StatusCode, string Description);
diff --git a/templates/Bff/src/WebApp/Program.cs b/templates/Bff/src/WebApp/Program.cs
new file mode 100644
index 00000000..177e8209
--- /dev/null
+++ b/templates/Bff/src/WebApp/Program.cs
@@ -0,0 +1,66 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Infrastructure;
+using Microsoft.AspNetCore.Http.Json;
+using Microsoft.AspNetCore.ResponseCompression;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Scalar.AspNetCore;
+using WebApp.Endpoints;
+using WebApp.Extensions;
+using WebApp.Middlewares;
+
+namespace WebApp;
+
+#pragma warning disable S1118 // Utility classes should not have public constructors
+public sealed class Program
+#pragma warning restore S1118 // Utility classes should not have public constructors
+{
+ private static async Task Main(string[] args)
+ {
+ var builder = WebApplication.CreateBuilder(args);
+
+ builder.Services.AddEndpointsApiExplorer();
+ builder.Services.AddOpenApi();
+ builder.Services.AddCustomHealthChecks(builder.Configuration);
+ builder.Services.AddRateLimiting(builder.Configuration);
+ builder.Services.AddResponseCompression(
+ options =>
+ {
+ options.EnableForHttps = true;
+ options.MimeTypes = ResponseCompressionDefaults.MimeTypes;
+ options.Providers.Add();
+ }
+ );
+ builder.Services.Configure(options =>
+ {
+ options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
+ options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
+ options.SerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip;
+ options.SerializerOptions.PropertyNameCaseInsensitive = true;
+ });
+
+ builder.AddInfrastructure();
+
+ builder.WebHost.ConfigureKestrel(options =>
+ options.ConfigureEndpointDefaults(listenOptions =>
+ listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3
+ ));
+
+ var app = builder.Build();
+
+ app.MapOpenApi();
+ app.MapScalarApiReference();
+
+ app.UseHttpsRedirection();
+
+ if (app.Configuration.GetValue("RATE_LIMITING_ENABLED"))
+ app.UseRateLimiter();
+
+ app.MapEndpoints()
+ .UseCustomHealthChecks()
+ .UseResponseCompression()
+ .UseMiddleware();
+
+ await app.RunAsync();
+ }
+}
diff --git a/templates/Bff/src/WebApp/Properties/launchSettings.json b/templates/Bff/src/WebApp/Properties/launchSettings.json
new file mode 100644
index 00000000..57889df9
--- /dev/null
+++ b/templates/Bff/src/WebApp/Properties/launchSettings.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "Development": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "https://*:7176;http://*:5011",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "RATE_LIMITING_ENABLED": "true",
+ "ENABLE_SENSITIVE_DATA_LOGGING": "true",
+ "OTEL_SERVICE_NAME": "Hexagonal.Solution.Template.WebApp.Bff",
+ "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317",
+ "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
+ "OTEL_RESOURCE_ATTRIBUTES": "service.namespace=development",
+ "ConnectionStrings__Redis": "127.0.0.1:6379"
+
+ }
+ }
+ }
+}
diff --git a/templates/Bff/src/WebApp/WebApp.csproj b/templates/Bff/src/WebApp/WebApp.csproj
new file mode 100644
index 00000000..f4aeb591
--- /dev/null
+++ b/templates/Bff/src/WebApp/WebApp.csproj
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/Bff/src/WebApp/WebApp.http b/templates/Bff/src/WebApp/WebApp.http
new file mode 100644
index 00000000..70b3239b
--- /dev/null
+++ b/templates/Bff/src/WebApp/WebApp.http
@@ -0,0 +1,48 @@
+@WebApp_HostAddress = https://localhost:7176
+
+###
+GET {{WebApp_HostAddress}}/health HTTP/2
+Accept: application/json
+
+###
+GET {{WebApp_HostAddress}}/live HTTP/2
+
+###
+GET {{WebApp_HostAddress}}/ready HTTP/2
+
+###
+GET {{WebApp_HostAddress}}/orders/1 HTTP/2
+CorrelationId: {{$guid}}
+CacheEnabled: false
+Accept-Encoding: gzip, deflate
+Accept: application/json
+
+###
+GET {{WebApp_HostAddress}}/orders/4 HTTP/2
+CorrelationId: {{$guid}}
+CacheEnabled: false
+Accept-Encoding: gzip, deflate
+Accept: application/json
+
+
+###
+POST {{WebApp_HostAddress}}/orders HTTP/2
+Content-Type: application/json
+
+{
+ "CorrelationId": "{{$guid}}",
+ "Description": "John's computer",
+ "Items": [
+ {
+ "Name": "Computer",
+ "Description": "Surface 2",
+ "Value": 1000
+ },
+ {
+ "Name": "Mouse",
+ "Description": "Microsoft mouse",
+ "Value": 99
+ }
+ ]
+}
+
diff --git a/templates/Bff/src/WebApp/appsettings.Development.json b/templates/Bff/src/WebApp/appsettings.Development.json
new file mode 100644
index 00000000..f7738e78
--- /dev/null
+++ b/templates/Bff/src/WebApp/appsettings.Development.json
@@ -0,0 +1,34 @@
+{
+ "ConnectionStrings": {
+ "Redis": "localhost:6379"
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "Services": [
+ {
+ "Name": "Orders",
+ "BaseAddress": "http://localhost:5012",
+ "Headers": {
+ "Accept": "application/json",
+ "Accept-Encoding": "gzip, deflate, br"
+ },
+ "LimitPerMinute": 500,
+ "ProtocolVersion": 2
+ },
+ {
+ "Name": "Payments",
+ "BaseAddress": "http://localhost:5012",
+ "Headers": {
+ "Accept": "application/json",
+ "Accept-Encoding": "gzip, deflate, br"
+ },
+ "LimitPerMinute": 500,
+ "ProtocolVersion": 2
+ }
+ ]
+}
\ No newline at end of file
diff --git a/templates/Bff/src/WebApp/appsettings.json b/templates/Bff/src/WebApp/appsettings.json
new file mode 100644
index 00000000..c28e77bb
--- /dev/null
+++ b/templates/Bff/src/WebApp/appsettings.json
@@ -0,0 +1,34 @@
+{
+ "ConnectionStrings": {
+ "Redis": "redis:6379"
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "Services": [
+ {
+ "Name": "Orders",
+ "BaseAddress": "http://mock-api:5012",
+ "Headers": {
+ "Accept": "application/json",
+ "Accept-Encoding": "gzip, deflate, br"
+ },
+ "LimitPerMinute": 500,
+ "ProtocolVersion": 2
+ },
+ {
+ "Name": "Payments",
+ "BaseAddress": "http://mock-api:5012",
+ "Headers": {
+ "Accept": "application/json",
+ "Accept-Encoding": "gzip, deflate, br"
+ },
+ "LimitPerMinute": 500,
+ "ProtocolVersion": 2
+ }
+ ]
+}
\ No newline at end of file
diff --git a/templates/Bff/src/WebApp/packages.lock.json b/templates/Bff/src/WebApp/packages.lock.json
new file mode 100644
index 00000000..6d7e1f26
--- /dev/null
+++ b/templates/Bff/src/WebApp/packages.lock.json
@@ -0,0 +1,311 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "AspNetCore.HealthChecks.Redis": {
+ "type": "Direct",
+ "requested": "[9.0.0, )",
+ "resolved": "9.0.0",
+ "contentHash": "yNH0h8GLRbAf+PU5HNVLZ5hNeyq9mDVmRKO9xuZsme/znUYoBJlQvI0gq45gaZNlLncCHkMhR4o90MuT+gxxPw==",
+ "dependencies": {
+ "StackExchange.Redis": "2.7.4"
+ }
+ },
+ "AspNetCore.HealthChecks.UI.Client": {
+ "type": "Direct",
+ "requested": "[9.0.0, )",
+ "resolved": "9.0.0",
+ "contentHash": "1Ub3Wvvbz7CMuFNWgLEc9qqQibiMoovDML/WHrwr5J83RPgtI20giCR92s/ipLgu7IIuqw+W/y7WpIeHqAICxg==",
+ "dependencies": {
+ "AspNetCore.HealthChecks.UI.Core": "9.0.0"
+ }
+ },
+ "AspNetCore.HealthChecks.Uris": {
+ "type": "Direct",
+ "requested": "[9.0.0, )",
+ "resolved": "9.0.0",
+ "contentHash": "XYdNlA437KeF8p9qOpZFyNqAN+c0FXt/JjTvzH/Qans0q0O3pPE8KPnn39ucQQjR/Roum1vLTP3kXiUs8VHyuA=="
+ },
+ "Microsoft.AspNetCore.OpenApi": {
+ "type": "Direct",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "vTcxIfOPyfFbYk1g8YcXJfkMnlEWVkSnnjxcZLy60zgwiHMRf2SnZR+9E4HlpwKxgE3yfKMOti8J6WfKuKsw6w==",
+ "dependencies": {
+ "Microsoft.OpenApi": "2.0.0"
+ }
+ },
+ "Scalar.AspNetCore": {
+ "type": "Direct",
+ "requested": "[2.13.11, )",
+ "resolved": "2.13.11",
+ "contentHash": "bH99KIEEaYhC+mMM9011OJtou0y/9O2NXo6h9/k104sAniMzFSGKNaiIX6NRkxc487MJD8vYu3I3nJtW/nU/3g=="
+ },
+ "AspNetCore.HealthChecks.UI.Core": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "TVriy4hgYnhfqz6NAzv8qe62Q8wf82iKUL6WV9selqeFZTq1ILi39Sic6sFQegRysvAVcnxKP/vY8z9Fk8x6XQ=="
+ },
+ "Google.Protobuf": {
+ "type": "Transitive",
+ "resolved": "3.31.1",
+ "contentHash": "gSnJbUmGiOTdWddPhqzrEscHq9Ls6sqRDPB9WptckyjTUyx70JOOAaDLkFff8gManZNN3hllQ4aQInnQyq/Z/A=="
+ },
+ "Grpc.AspNetCore.Server": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "diSC/ZeNdSdxHdYSOpYwuSBBDYpuNVtJQFJfiBB0WrYOQ4lVMmdxuUZJcViahQyo8pCvS3Mueo5lqFxwwMF/iw==",
+ "dependencies": {
+ "Grpc.Net.Common": "2.76.0"
+ }
+ },
+ "Grpc.AspNetCore.Server.ClientFactory": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "y5KGO1GO0N2L/hCCMR05mmoK8j+v8rKvZ+9nothAxKx2Tf2CwV8f4TM5K0GkKfDsp4vrc4lm90MU6E+DeN7YIw==",
+ "dependencies": {
+ "Grpc.AspNetCore.Server": "2.76.0",
+ "Grpc.Net.ClientFactory": "2.76.0"
+ }
+ },
+ "Grpc.Core.Api": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "cSxC2tdnFdXXuBgIn1pjc4YBx7LXTCp4M0qn+SMBS35VWZY+cEQYLWTBDDhdBH1HzU7BV+ncVZlniGQHMpRJKQ=="
+ },
+ "Grpc.Net.Client": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "K1oldmqw2+Gn69nGRzZLhqSiUZwelX1GrBu/cUl9wNf1C0uB61vFS6JcxUUv9P8VoUJhFsmV44JA6lI2EUt4xw==",
+ "dependencies": {
+ "Grpc.Net.Common": "2.76.0"
+ }
+ },
+ "Grpc.Net.ClientFactory": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "XI+kO69L9AV8B9N0UQOmH911r6MOEp9huHiavEsY56DJYuzJ9KAxNGy37dpV6CLbgCaN2uKmpOsZ9Pao6bmpVQ==",
+ "dependencies": {
+ "Grpc.Net.Client": "2.76.0"
+ }
+ },
+ "Grpc.Net.Common": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "bZpiMVYgvpB44/wBh1RotrkqC7bg2FOasLri2GhR3hMKyzsiTxCoDE49YjPrJeFc4RW0wS8u+EInI09sjxVFRA==",
+ "dependencies": {
+ "Grpc.Core.Api": "2.76.0"
+ }
+ },
+ "Grpc.Tools": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "goRzYZVMgQtyLvkWdgTnPycg49hlNxnMkqGGgR3l7nCOm0bUh0YeAneiJ9JFk3XLgF4suQUdETYkl2Mg/TBr0w=="
+ },
+ "Microsoft.OpenApi": {
+ "type": "Transitive",
+ "resolved": "2.0.0",
+ "contentHash": "GGYLfzV/G/ct80OZ45JxnWP7NvMX1BCugn/lX7TH5o0lcVaviavsLMTxmFV2AybXWjbi3h6FF1vgZiTK6PXndw=="
+ },
+ "OpenTelemetry": {
+ "type": "Transitive",
+ "resolved": "1.15.0",
+ "contentHash": "7mS/oZFF8S6xyqGQfMU1btp0nXJQUPWV535Vp/XMLYwRAUv36xQN+U4vufWBF1+z4HnRTOwuFHtUSGnHbyN6FQ==",
+ "dependencies": {
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.0"
+ }
+ },
+ "OpenTelemetry.Api": {
+ "type": "Transitive",
+ "resolved": "1.15.0",
+ "contentHash": "vk5OGdf6K9kQScCWo3bRjhDWCv6Pqw92IpX4dlARZ8B1WL7/2NGTDtCkkw42eQf7UdwyoHKzVvMH/PtL8d6z7w=="
+ },
+ "OpenTelemetry.Api.ProviderBuilderExtensions": {
+ "type": "Transitive",
+ "resolved": "1.15.0",
+ "contentHash": "OnuSUlRpGvowkOzGFQfy+KZFu0cITfKfh2IYJJiZskxVJiOuexwOOuvfDAgpJdmTzVWAHjYdz2shcHZaJ06UjQ==",
+ "dependencies": {
+ "OpenTelemetry.Api": "1.15.0"
+ }
+ },
+ "Pipelines.Sockets.Unofficial": {
+ "type": "Transitive",
+ "resolved": "2.2.8",
+ "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ=="
+ },
+ "Polly": {
+ "type": "Transitive",
+ "resolved": "7.2.4",
+ "contentHash": "bw00Ck5sh6ekduDE3mnCo1ohzuad946uslCDEENu3091+6UKnBuKLo4e+yaNcCzXxOZCXWY2gV4a35+K1d4LDA=="
+ },
+ "Polly.Extensions.Http": {
+ "type": "Transitive",
+ "resolved": "3.0.0",
+ "contentHash": "drrG+hB3pYFY7w1c3BD+lSGYvH2oIclH8GRSehgfyP5kjnFnHKQuuBhuHLv+PWyFuaTDyk/vfRpnxOzd11+J8g==",
+ "dependencies": {
+ "Polly": "7.1.0"
+ }
+ },
+ "StackExchange.Redis": {
+ "type": "Transitive",
+ "resolved": "2.7.27",
+ "contentHash": "Uqc2OQHglqj9/FfGQ6RkKFkZfHySfZlfmbCl+hc+u2I/IqunfelQ7QJi7ZhvAJxUtu80pildVX6NPLdDaUffOw==",
+ "dependencies": {
+ "Pipelines.Sockets.Unofficial": "2.2.8"
+ }
+ },
+ "contracts": {
+ "type": "Project",
+ "dependencies": {
+ "Grpc.AspNetCore": "[2.76.0, )"
+ }
+ },
+ "infrastructure": {
+ "type": "Project",
+ "dependencies": {
+ "Contracts": "[1.0.0, )",
+ "Microsoft.Extensions.Caching.Hybrid": "[10.4.0, )",
+ "Microsoft.Extensions.Caching.StackExchangeRedis": "[10.0.5, )",
+ "Microsoft.Extensions.Http.Polly": "[10.0.5, )",
+ "OpenTelemetry.Exporter.Console": "[1.15.0, )",
+ "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.0, )",
+ "OpenTelemetry.Extensions": "[1.14.0-beta.1, )",
+ "OpenTelemetry.Extensions.Hosting": "[1.15.0, )",
+ "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.1, )",
+ "OpenTelemetry.Instrumentation.GrpcNetClient": "[1.15.0-beta.1, )",
+ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )",
+ "OpenTelemetry.Instrumentation.Process": "[1.15.0-beta.1, )",
+ "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )",
+ "OpenTelemetry.Instrumentation.StackExchangeRedis": "[1.15.0-beta.1, )"
+ }
+ },
+ "Grpc.AspNetCore": {
+ "type": "CentralTransitive",
+ "requested": "[2.76.0, )",
+ "resolved": "2.76.0",
+ "contentHash": "LyXMmpN2Ba0TE35SOLSKbGqIYtJuhc1UgiaGfoW1X8KJERV70QI5KGW+ckEY7MrXoFWN/uWo4B70siVhbDmCgQ==",
+ "dependencies": {
+ "Google.Protobuf": "3.31.1",
+ "Grpc.AspNetCore.Server.ClientFactory": "2.76.0",
+ "Grpc.Tools": "2.76.0"
+ }
+ },
+ "Microsoft.Extensions.Caching.Hybrid": {
+ "type": "CentralTransitive",
+ "requested": "[10.4.0, )",
+ "resolved": "10.4.0",
+ "contentHash": "4V+aMLQeU/p4VcIWIcvGro0L6HynmL2TrelL04Ce1iotP6T5+kjxuZQvl6P1ObSXIRPCbVXtQSt1NxK0fRIuag=="
+ },
+ "Microsoft.Extensions.Caching.StackExchangeRedis": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "zXb143/TpEKOLQuWGw2CkJgb9F4XXh2XbevMvppzsIHr1/pjML0zjc+vzXcpCV8YUwpW5NIaScZhzFSm621B3Q==",
+ "dependencies": {
+ "StackExchange.Redis": "2.7.27"
+ }
+ },
+ "Microsoft.Extensions.Http.Polly": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "4VcH+2eVBQw3MtRpo02nrhv/nU54tL/pkcRF0fSSwD+8MoxgE1EjylPKbSIqHEK7iiB17I0iB37Ao8y+q1sV8g==",
+ "dependencies": {
+ "Polly": "7.2.4",
+ "Polly.Extensions.Http": "3.0.0"
+ }
+ },
+ "OpenTelemetry.Exporter.Console": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "Jweov3Q70xmy5U8bwab8xd+xAuaFBI4695q/IpH4/dcAwKytyB+WhV5HufmKfXiKZhRbSEo8piG+i1ENEmdFXw==",
+ "dependencies": {
+ "OpenTelemetry": "1.15.0"
+ }
+ },
+ "OpenTelemetry.Exporter.OpenTelemetryProtocol": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "VH8ANc/js9IRvfYt0Q2UaAxNCOWm+IU+vWrtoH7pfx4oWPVdISUt+9uWfBCFMWZg5WzQip5dhslyDjeyZXXfSQ==",
+ "dependencies": {
+ "OpenTelemetry": "1.15.0"
+ }
+ },
+ "OpenTelemetry.Extensions": {
+ "type": "CentralTransitive",
+ "requested": "[1.14.0-beta.1, )",
+ "resolved": "1.14.0-beta.1",
+ "contentHash": "4DxYvBgz3OaCD8DE2lutiMiaEZh/PvWW05ewhAeqS6lKAETultAzfCveDXxMcIkaVwc93PR/Z22/P5zZ2rmHZA==",
+ "dependencies": {
+ "OpenTelemetry": "[1.14.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Extensions.Hosting": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "RixjKyB1pbYGhWdvPto4KJs+exdQknJsnjUO9WszdLles5Vcd0EYzxPNJdwmLjYfP+Jfbr4B5nktM4ZgeHSWtg==",
+ "dependencies": {
+ "OpenTelemetry": "1.15.0"
+ }
+ },
+ "OpenTelemetry.Instrumentation.AspNetCore": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.1, )",
+ "resolved": "1.15.1",
+ "contentHash": "wXaZTu6LHY8xcbRd6ClcrtjHqGVoGYCcArXEZA3iUjUcYSVYwDGyPU0PdkwTfylxv8JeCCVDQhVb0fT7xBJjGA==",
+ "dependencies": {
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.GrpcNetClient": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.0-beta.1, )",
+ "resolved": "1.15.0-beta.1",
+ "contentHash": "SBas5+C4kGUqoy8OPpQis+QIgJ7/aaJl4H3oLzHCJnZLCb8TXZmQL2/r753RXXJUH8oIeLIzdW+EXgujSy+cpQ==",
+ "dependencies": {
+ "OpenTelemetry": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.Http": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "uToc7bUp8IEdb0ny9mKsL6FrrYelINPzxxiSShJgOf4XmQc4Azww6S5RjRj24YhsOn2a1MABOrxfVTZXtDk4Eg==",
+ "dependencies": {
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.Process": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.0-beta.1, )",
+ "resolved": "1.15.0-beta.1",
+ "contentHash": "99zSvpwsMpKextd3RkHGY8iRsmw1qg3PjqkjC5hZI0fZG6m+wPsssrX6z9RhwDGcZ8sdGNjoKyLpNOmR154zQg==",
+ "dependencies": {
+ "OpenTelemetry.Api": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.Runtime": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "OOvpqR/j2Pb6+tWhHNODIbSJ53Or/MDtTiXEyrsWI02K2lLAgvBFcxUOrHggS/8015cYR3AdSaXv6NZrkz5yQA==",
+ "dependencies": {
+ "OpenTelemetry.Api": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.StackExchangeRedis": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.0-beta.1, )",
+ "resolved": "1.15.0-beta.1",
+ "contentHash": "Igg/3MlBZZ9lZCTzMcvoFKav263+zOcKx9s4LVIdq96YmBHCuPmDiyygAIPdeIVzwN08VwD3RG1nXHDuRF1Ssg==",
+ "dependencies": {
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)",
+ "StackExchange.Redis": "2.6.122"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/templates/tests/CommonTests/CommonTests.csproj b/templates/Bff/tests/CommonTests/CommonTests.csproj
similarity index 100%
rename from templates/tests/CommonTests/CommonTests.csproj
rename to templates/Bff/tests/CommonTests/CommonTests.csproj
diff --git a/templates/Bff/tests/CommonTests/packages.lock.json b/templates/Bff/tests/CommonTests/packages.lock.json
new file mode 100644
index 00000000..7d97c3a9
--- /dev/null
+++ b/templates/Bff/tests/CommonTests/packages.lock.json
@@ -0,0 +1,57 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "AutoFixture": {
+ "type": "Direct",
+ "requested": "[4.18.1, )",
+ "resolved": "4.18.1",
+ "contentHash": "BmWZDY4fkrYOyd5/CTBOeXbzsNwV8kI4kDi/Ty1Y5F+WDHBVKxzfWlBE4RSicvZ+EOi2XDaN5uwdrHsItLW6Kw==",
+ "dependencies": {
+ "Fare": "[2.1.1, 3.0.0)"
+ }
+ },
+ "Newtonsoft.Json": {
+ "type": "Direct",
+ "requested": "[13.0.4, )",
+ "resolved": "13.0.4",
+ "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A=="
+ },
+ "xunit.extensibility.core": {
+ "type": "Direct",
+ "requested": "[2.9.3, )",
+ "resolved": "2.9.3",
+ "contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==",
+ "dependencies": {
+ "xunit.abstractions": "2.0.3"
+ }
+ },
+ "Fare": {
+ "type": "Transitive",
+ "resolved": "2.1.1",
+ "contentHash": "HaI8puqA66YU7/9cK4Sgbs1taUTP1Ssa4QT2PIzqJ7GvAbN1QgkjbRsjH+FSbMh1MJdvS0CIwQNLtFT+KF6KpA==",
+ "dependencies": {
+ "NETStandard.Library": "1.6.1"
+ }
+ },
+ "Microsoft.NETCore.Platforms": {
+ "type": "Transitive",
+ "resolved": "1.1.0",
+ "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
+ },
+ "NETStandard.Library": {
+ "type": "Transitive",
+ "resolved": "1.6.1",
+ "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.0"
+ }
+ },
+ "xunit.abstractions": {
+ "type": "Transitive",
+ "resolved": "2.0.3",
+ "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg=="
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/templates/Bff/tests/IntegrationTests/Common/CustomWebApplicationFactory.cs b/templates/Bff/tests/IntegrationTests/Common/CustomWebApplicationFactory.cs
new file mode 100644
index 00000000..37aa31f5
--- /dev/null
+++ b/templates/Bff/tests/IntegrationTests/Common/CustomWebApplicationFactory.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Mvc.Testing;
+using WebApp;
+
+namespace IntegrationTests.Common;
+
+[CollectionDefinition("WebApplicationFactoryCollectionDefinition")]
+public sealed class WebApplicationFactoryCollectionDefinition : IClassFixture>;
+
+public class CustomWebApplicationFactory : WebApplicationFactory, IDisposable where TProgram : class
+{
+}
diff --git a/templates/Bff/tests/IntegrationTests/Fixtures/BaseFixture.cs b/templates/Bff/tests/IntegrationTests/Fixtures/BaseFixture.cs
new file mode 100644
index 00000000..2e63ec18
--- /dev/null
+++ b/templates/Bff/tests/IntegrationTests/Fixtures/BaseFixture.cs
@@ -0,0 +1,6 @@
+namespace IntegrationTests.Fixtures;
+public class BaseFixture
+{
+
+ public CancellationToken CancellationToken { get; } = CancellationToken.None;
+}
diff --git a/templates/Bff/tests/IntegrationTests/GlobalUsings.cs b/templates/Bff/tests/IntegrationTests/GlobalUsings.cs
new file mode 100644
index 00000000..9df1d421
--- /dev/null
+++ b/templates/Bff/tests/IntegrationTests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Xunit;
diff --git a/templates/Bff/tests/IntegrationTests/IntegrationTests.csproj b/templates/Bff/tests/IntegrationTests/IntegrationTests.csproj
new file mode 100644
index 00000000..2be9c611
--- /dev/null
+++ b/templates/Bff/tests/IntegrationTests/IntegrationTests.csproj
@@ -0,0 +1,27 @@
+
+
+ false
+ true
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/Bff/tests/IntegrationTests/WebApp/Grpc/ApiGrpcHelper.cs b/templates/Bff/tests/IntegrationTests/WebApp/Grpc/ApiGrpcHelper.cs
new file mode 100644
index 00000000..2d284095
--- /dev/null
+++ b/templates/Bff/tests/IntegrationTests/WebApp/Grpc/ApiGrpcHelper.cs
@@ -0,0 +1,12 @@
+// using Grpc.Net.Client;
+
+// namespace IntegrationTests.WebApp.Grpc;
+// public sealed class ApiGrpcHelper(HttpClient httpClient)
+// {
+// public HttpClient httpClient = httpClient;
+
+// public GrpcChannel AsGrpcClientChannel() => GrpcChannel.ForAddress(httpClient.BaseAddress!, new GrpcChannelOptions
+// {
+// HttpClient = httpClient
+// });
+// }
diff --git a/templates/Bff/tests/IntegrationTests/WebApp/Grpc/GetOrderGrpcTest.cs b/templates/Bff/tests/IntegrationTests/WebApp/Grpc/GetOrderGrpcTest.cs
new file mode 100644
index 00000000..84c76b37
--- /dev/null
+++ b/templates/Bff/tests/IntegrationTests/WebApp/Grpc/GetOrderGrpcTest.cs
@@ -0,0 +1,70 @@
+// using Grpc.Net.Client;
+// using GrpcOrder;
+// using IntegrationTests.Common;
+// using IntegrationTests.Fixtures;
+// using IntegrationTests.WebApp.Grpc.Common;
+// using WebApp;
+
+// namespace IntegrationTests.WebApp.Grpc.Orders;
+
+// [Collection("WebApplicationFactoryCollectionDefinition")]
+// public class GetOrderGrpcTest : BaseFixture
+// {
+// public CustomWebApplicationFactory customWebApplicationFactory;
+
+// public ApiGrpcHelper apiGrpcHelper;
+// private readonly GrpcChannel _grpcChannel;
+// private readonly OrderService.OrderServiceClient _service;
+
+// public GetOrderGrpcTest(CustomWebApplicationFactory customWebApplicationFactory)
+// {
+// this.customWebApplicationFactory = customWebApplicationFactory;
+// apiGrpcHelper = new(this.customWebApplicationFactory.CreateClient());
+// _grpcChannel = apiGrpcHelper.AsGrpcClientChannel();
+// _service = new(_grpcChannel);
+// }
+
+// [Fact(DisplayName = nameof(Given_A_Valid_Request_Then_Pass))]
+// public async Task Given_A_Valid_Request_Then_Pass()
+// {
+// // Arrange
+// GetOrderRequest request = new()
+// {
+// CorrelationId = Guid.NewGuid().ToString(),
+// Id = 1
+// };
+
+// // Act
+// var response = await _service.GetAsync(request);
+
+// // Assert
+// Assert.NotNull(response);
+// Assert.True(response.Success);
+// Assert.True(string.IsNullOrEmpty(response.Message));
+// Assert.NotNull(response.Data);
+// Assert.Equal(1, response.Data.Id);
+// Assert.NotNull(response.Data.Items);
+// Assert.NotEmpty(response.Data.Items);
+// Assert.Equal(1000.0, response.Data.Total);
+// }
+
+// [Fact(DisplayName = nameof(Given_A_Invalid_Request_Then_Fails))]
+// public async Task Given_A_Invalid_Request_Then_Fails()
+// {
+// // Arrange
+// GetOrderRequest request = new()
+// {
+// CorrelationId = Guid.NewGuid().ToString(),
+// Id = 999
+// };
+
+// // Act
+// var response = await _service.GetAsync(request);
+
+// // Assert
+// Assert.NotNull(response);
+// Assert.False(response.Success);
+// Assert.False(string.IsNullOrEmpty(response.Message));
+// Assert.Null(response.Data);
+// }
+// }
diff --git a/templates/Bff/tests/IntegrationTests/WebApp/Http/ApiHelper.cs b/templates/Bff/tests/IntegrationTests/WebApp/Http/ApiHelper.cs
new file mode 100644
index 00000000..eb2be9ac
--- /dev/null
+++ b/templates/Bff/tests/IntegrationTests/WebApp/Http/ApiHelper.cs
@@ -0,0 +1,60 @@
+using System.Text;
+using System.Text.Json;
+using Grpc.Net.Client;
+
+namespace IntegrationTests.WebApp.Http;
+public sealed class ApiHelper(HttpClient httpClient)
+{
+ readonly HttpClient _httpClient = httpClient;
+
+ private static readonly JsonSerializerOptions _jsonSerializerOptions = new()
+ {
+ PropertyNameCaseInsensitive = true
+ };
+
+ public void AddHeaders(Dictionary headers)
+ {
+ foreach (var header in headers)
+ {
+ if (_httpClient.DefaultRequestHeaders.Contains(header.Key))
+ {
+ _httpClient.DefaultRequestHeaders.Remove(header.Key);
+ }
+
+ _httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
+ }
+ }
+
+ public async Task GetAsync(string resourceUrl) =>
+ await _httpClient.GetAsync(resourceUrl);
+
+ public async Task PostAsync(string resourceUrl, dynamic dataClass) =>
+ await _httpClient.PostAsync(resourceUrl, SerializeRequest(dataClass));
+
+ public async Task PutAsync(string resourceUrl, dynamic data) =>
+ await _httpClient.PutAsync(resourceUrl, SerializeRequest(data));
+
+ public async Task DeleteAsync(string resourceUrl) =>
+ await _httpClient.DeleteAsync(resourceUrl);
+
+ public static StringContent SerializeRequest(dynamic data)
+ {
+ var json = JsonSerializer.Serialize(data);
+ return new StringContent(json, Encoding.UTF8, "application/json");
+ }
+
+ public static async Task DeSerializeResponse(HttpResponseMessage response)
+ {
+ if (response.Content == null)
+ return default;
+
+ var content = await response.Content.ReadAsStreamAsync();
+
+ return JsonSerializer.Deserialize(content, _jsonSerializerOptions);
+ }
+
+ public GrpcChannel AsGrpcClientChannel() => GrpcChannel.ForAddress(_httpClient.BaseAddress!, new GrpcChannelOptions
+ {
+ HttpClient = _httpClient
+ });
+}
diff --git a/templates/Bff/tests/IntegrationTests/WebApp/Http/BaseHttpFixture.cs b/templates/Bff/tests/IntegrationTests/WebApp/Http/BaseHttpFixture.cs
new file mode 100644
index 00000000..c251bb7d
--- /dev/null
+++ b/templates/Bff/tests/IntegrationTests/WebApp/Http/BaseHttpFixture.cs
@@ -0,0 +1,14 @@
+using IntegrationTests.Common;
+using IntegrationTests.Fixtures;
+using WebApp;
+
+namespace IntegrationTests.WebApp.Http;
+
+public class BaseHttpFixture : BaseFixture
+{
+ public ApiHelper ApiHelper { get; set; } = null!;
+ public string ResourceUrl { get; set; } = string.Empty;
+
+ public void SetApiHelper(CustomWebApplicationFactory customWebApplicationFactory) =>
+ ApiHelper = new(customWebApplicationFactory.CreateClient());
+}
\ No newline at end of file
diff --git a/templates/Bff/tests/IntegrationTests/WebApp/Http/OrderTest.cs b/templates/Bff/tests/IntegrationTests/WebApp/Http/OrderTest.cs
new file mode 100644
index 00000000..d8c77673
--- /dev/null
+++ b/templates/Bff/tests/IntegrationTests/WebApp/Http/OrderTest.cs
@@ -0,0 +1,89 @@
+using System.Net;
+using Contracts.Common;
+using Contracts.Orders;
+using IntegrationTests.Common;
+using WebApp;
+
+namespace IntegrationTests.WebApp.Http;
+
+[Collection("WebApplicationFactoryCollectionDefinition")]
+public sealed class OrderTest : IClassFixture
+{
+ private readonly BaseHttpFixture _fixture;
+ public OrderTest(CustomWebApplicationFactory customWebApplicationFactory, BaseHttpFixture fixture)
+ {
+ _fixture = fixture;
+ _fixture.SetApiHelper(customWebApplicationFactory);
+ _fixture.ResourceUrl = "orders/{0}";
+ }
+
+ [Fact(DisplayName = nameof(GivenAGetByIdValidRequestThenPass))]
+ public async Task GivenAGetByIdValidRequestThenPass()
+ {
+ // Arrange
+ var id = 1;
+ var url = string.Format(System.Globalization.CultureInfo.InvariantCulture, _fixture.ResourceUrl, id);
+ _fixture.ApiHelper.AddHeaders(new Dictionary
+ {
+ { "CorrelationId", Guid.NewGuid().ToString() },
+ { "CacheEnabled", "false" }
+ });
+
+ // Act
+ var result = await _fixture.ApiHelper.GetAsync(url);
+ var response = await ApiHelper.DeSerializeResponse>(result);
+ var data = response?.Data;
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(HttpStatusCode.OK, result.StatusCode);
+ Assert.NotNull(data);
+ Assert.NotNull(data.Items);
+ Assert.NotEmpty(data.Items);
+ }
+
+ [Fact(DisplayName = nameof(GivenAGetByIdInvalidRequestThenFails))]
+ public async Task GivenAGetByIdInvalidRequestThenFails()
+ {
+ // Arrange
+ var id = 9999999;
+ var url = string.Format(System.Globalization.CultureInfo.InvariantCulture, _fixture.ResourceUrl, id);
+ _fixture.ApiHelper.AddHeaders(new Dictionary
+ {
+ { "CorrelationId", Guid.NewGuid().ToString() },
+ { "CacheEnabled", "false" }
+ });
+
+ // Act
+ var result = await _fixture.ApiHelper.GetAsync(url);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(HttpStatusCode.NotFound, result.StatusCode);
+ Assert.False(result.IsSuccessStatusCode);
+ }
+
+ [Fact(DisplayName = nameof(GivenAValidCreateRequestThenPass))]
+ public async Task GivenAValidCreateRequestThenPass()
+ {
+ // Arrange
+ var url = string.Format(System.Globalization.CultureInfo.InvariantCulture, _fixture.ResourceUrl, string.Empty);
+ CreateOrderRequest request = new(Guid.NewGuid(), "Test Order",
+ [
+ new("Item 1", "Description 1", 500.0m),
+ new("Item 2", "Description 2", 500.0m)
+ ]);
+
+ // Act
+ var result = await _fixture.ApiHelper.PostAsync(url, request);
+ var response = await ApiHelper.DeSerializeResponse>(result);
+ var data = response?.Data;
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(HttpStatusCode.Created, result.StatusCode);
+ Assert.NotNull(data);
+ Assert.NotNull(data.Items);
+ Assert.NotEmpty(data.Items);
+ }
+}
diff --git a/templates/Bff/tests/IntegrationTests/packages.lock.json b/templates/Bff/tests/IntegrationTests/packages.lock.json
new file mode 100644
index 00000000..59692e6f
--- /dev/null
+++ b/templates/Bff/tests/IntegrationTests/packages.lock.json
@@ -0,0 +1,807 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "coverlet.collector": {
+ "type": "Direct",
+ "requested": "[8.0.1, )",
+ "resolved": "8.0.1",
+ "contentHash": "heVQl5tKYnnIDYlR1QMVGueYH6iriZTcZB6AjDczQNwZzxkjDIt9C84Pt4cCiZYrbo7jkZOYGWbs6Lo9wAtVLg=="
+ },
+ "Microsoft.AspNetCore.Mvc.Testing": {
+ "type": "Direct",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "MfacYQ7jNzj6073YobyoFfXpNmGqrV1UCywTM339DOcYpfalcM4K4heFjV5k3dDkKkWOGWO/DV3hdmVRqFkIxA==",
+ "dependencies": {
+ "Microsoft.AspNetCore.TestHost": "10.0.5",
+ "Microsoft.Extensions.DependencyModel": "10.0.5",
+ "Microsoft.Extensions.Hosting": "10.0.5"
+ }
+ },
+ "Microsoft.AspNetCore.TestHost": {
+ "type": "Direct",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "PJEdrZnnhvxIEXzDdvdZ38GvpdaiUfKkZ99kudS8riJwhowFb/Qh26Wjk9smrCWcYdMFQmpN5epGiL4o1s8LYA=="
+ },
+ "Microsoft.NET.Test.Sdk": {
+ "type": "Direct",
+ "requested": "[18.3.0, )",
+ "resolved": "18.3.0",
+ "contentHash": "xW3kXuWRQtgoxJp4J+gdhHSQyK+6Wb/AZDSd7lMvuMRYlZ1tnpkojyfZlWilB5G4dmZ0Y0ZxU/M23TlubndNkw==",
+ "dependencies": {
+ "Microsoft.CodeCoverage": "18.3.0",
+ "Microsoft.TestPlatform.TestHost": "18.3.0"
+ }
+ },
+ "xunit": {
+ "type": "Direct",
+ "requested": "[2.9.3, )",
+ "resolved": "2.9.3",
+ "contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==",
+ "dependencies": {
+ "xunit.analyzers": "1.18.0",
+ "xunit.assert": "2.9.3",
+ "xunit.core": "[2.9.3]"
+ }
+ },
+ "xunit.runner.visualstudio": {
+ "type": "Direct",
+ "requested": "[3.1.5, )",
+ "resolved": "3.1.5",
+ "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA=="
+ },
+ "AspNetCore.HealthChecks.UI.Core": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "TVriy4hgYnhfqz6NAzv8qe62Q8wf82iKUL6WV9selqeFZTq1ILi39Sic6sFQegRysvAVcnxKP/vY8z9Fk8x6XQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11"
+ }
+ },
+ "Google.Protobuf": {
+ "type": "Transitive",
+ "resolved": "3.31.1",
+ "contentHash": "gSnJbUmGiOTdWddPhqzrEscHq9Ls6sqRDPB9WptckyjTUyx70JOOAaDLkFff8gManZNN3hllQ4aQInnQyq/Z/A=="
+ },
+ "Grpc.AspNetCore.Server": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "diSC/ZeNdSdxHdYSOpYwuSBBDYpuNVtJQFJfiBB0WrYOQ4lVMmdxuUZJcViahQyo8pCvS3Mueo5lqFxwwMF/iw==",
+ "dependencies": {
+ "Grpc.Net.Common": "2.76.0"
+ }
+ },
+ "Grpc.AspNetCore.Server.ClientFactory": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "y5KGO1GO0N2L/hCCMR05mmoK8j+v8rKvZ+9nothAxKx2Tf2CwV8f4TM5K0GkKfDsp4vrc4lm90MU6E+DeN7YIw==",
+ "dependencies": {
+ "Grpc.AspNetCore.Server": "2.76.0",
+ "Grpc.Net.ClientFactory": "2.76.0"
+ }
+ },
+ "Grpc.Core.Api": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "cSxC2tdnFdXXuBgIn1pjc4YBx7LXTCp4M0qn+SMBS35VWZY+cEQYLWTBDDhdBH1HzU7BV+ncVZlniGQHMpRJKQ=="
+ },
+ "Grpc.Net.Client": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "K1oldmqw2+Gn69nGRzZLhqSiUZwelX1GrBu/cUl9wNf1C0uB61vFS6JcxUUv9P8VoUJhFsmV44JA6lI2EUt4xw==",
+ "dependencies": {
+ "Grpc.Net.Common": "2.76.0",
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.0"
+ }
+ },
+ "Grpc.Net.ClientFactory": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "XI+kO69L9AV8B9N0UQOmH911r6MOEp9huHiavEsY56DJYuzJ9KAxNGy37dpV6CLbgCaN2uKmpOsZ9Pao6bmpVQ==",
+ "dependencies": {
+ "Grpc.Net.Client": "2.76.0",
+ "Microsoft.Extensions.Http": "8.0.0"
+ }
+ },
+ "Grpc.Net.Common": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "bZpiMVYgvpB44/wBh1RotrkqC7bg2FOasLri2GhR3hMKyzsiTxCoDE49YjPrJeFc4RW0wS8u+EInI09sjxVFRA==",
+ "dependencies": {
+ "Grpc.Core.Api": "2.76.0"
+ }
+ },
+ "Grpc.Tools": {
+ "type": "Transitive",
+ "resolved": "2.76.0",
+ "contentHash": "goRzYZVMgQtyLvkWdgTnPycg49hlNxnMkqGGgR3l7nCOm0bUh0YeAneiJ9JFk3XLgF4suQUdETYkl2Mg/TBr0w=="
+ },
+ "Microsoft.CodeCoverage": {
+ "type": "Transitive",
+ "resolved": "18.3.0",
+ "contentHash": "23BNy/vziREC20Wwhb50K7+kZe0m07KlLWDQv4qjJ9tt3QjpDpDIqJFrhYHmMEo9xDkuSp55U/8h4bMF7MiB+g=="
+ },
+ "Microsoft.Extensions.Caching.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Caching.Memory": {
+ "type": "Transitive",
+ "resolved": "10.0.4",
+ "contentHash": "CLLussNUMdSbyJOu4VBF7sqskHGB/5N1EcFzrqG/HsPATN8fCRUcfp0qns1VwkxKHwxrtYCh5FKe+kM81Q1PHA==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.4",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.4",
+ "Microsoft.Extensions.Options": "10.0.4",
+ "Microsoft.Extensions.Primitives": "10.0.4"
+ }
+ },
+ "Microsoft.Extensions.Configuration": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Configuration.CommandLine": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "or9fOLopMUTJOQVJ3bou4aD6PwvsiKf4kZC4EE5sRRKSkmh+wfk/LekJXRjAX88X+1JA9zHjDo+5fiQ7z3MY/A==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Configuration.EnvironmentVariables": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Configuration.FileExtensions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Physical": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Json": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Configuration.UserSecrets": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "fhdG6UV9lIp70QhNkVyaHciUVq25IPFkczheVJL9bIFvmnJ+Zghaie6dWkDbbVmxZlHl9gj3zTDxMxJs5zNhIA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Configuration.Json": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Physical": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA=="
+ },
+ "Microsoft.Extensions.DependencyModel": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg=="
+ },
+ "Microsoft.Extensions.Diagnostics": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "vAJHd4yOpmKoK+jBuYV7a3y+Ab9U4ARCc29b6qvMy276RgJFw9LFs0DdsPqOL3ahwzyrX7tM+i4cCxU/RX0qAg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Diagnostics.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Diagnostics.HealthChecks": {
+ "type": "Transitive",
+ "resolved": "8.0.11",
+ "contentHash": "zLgN22Zp9pk8RHlwssRTexw4+a6wqOnKWN+VejdPn5Yhjql4XiBhkFo35Nu8mmqHIk/UEmmCnMGLWq75aFfkOw==",
+ "dependencies": {
+ "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "8.0.11",
+ "Microsoft.Extensions.Hosting.Abstractions": "8.0.1",
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.2",
+ "Microsoft.Extensions.Options": "8.0.2"
+ }
+ },
+ "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.0.11",
+ "contentHash": "So3JUdRxozRjvQ3cxU6F3nI/i4emDnjane6yMYcJhvTTTu29ltlIdoXjkFGRceIWz8yKvuEpzXItZ0x5GvN2nQ=="
+ },
+ "Microsoft.Extensions.FileProviders.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.FileProviders.Physical": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==",
+ "dependencies": {
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileSystemGlobbing": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.FileSystemGlobbing": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw=="
+ },
+ "Microsoft.Extensions.Hosting": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "8i7e5IBdiKLNqt/+ciWrS8U95Rv5DClaaj7ulkZbimnCi4uREWd+lXzkp3joofFuIPOlAzV4AckxLTIELv2jdg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Configuration.Binder": "10.0.5",
+ "Microsoft.Extensions.Configuration.CommandLine": "10.0.5",
+ "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.5",
+ "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5",
+ "Microsoft.Extensions.Configuration.Json": "10.0.5",
+ "Microsoft.Extensions.Configuration.UserSecrets": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Diagnostics": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Physical": "10.0.5",
+ "Microsoft.Extensions.Hosting.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Configuration": "10.0.5",
+ "Microsoft.Extensions.Logging.Console": "10.0.5",
+ "Microsoft.Extensions.Logging.Debug": "10.0.5",
+ "Microsoft.Extensions.Logging.EventLog": "10.0.5",
+ "Microsoft.Extensions.Logging.EventSource": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Hosting.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Http": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "AiFvHYM8nP0wPC7bGPI3NHQlSYSLqjjT7DMJUuuxhd+7pz3O89iu2gdQfgACy5DxsXENiok5i1bMacJL7KR8jA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Diagnostics": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Logging.Configuration": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Configuration.Binder": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Logging.Console": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Configuration": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Logging.Debug": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Logging.EventLog": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "0ezhWYJS4/6KrqQel9JL+Tr4n+4EX2TF5EYiaysBWNNEM2c3Gtj1moD39esfgk8OHblSX+UFjtZ3z0c4i9tRvw==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "System.Diagnostics.EventLog": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Logging.EventSource": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "vN+aq1hBFXyYvY5Ow9WyeR66drKQxRZmas4lAjh6QWfryPkjTn1uLtX5AFIxyDaZj78v5TG2sELUyvrXpAPQQw==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Options": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Options.ConfigurationExtensions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Configuration.Binder": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g=="
+ },
+ "Microsoft.OpenApi": {
+ "type": "Transitive",
+ "resolved": "2.0.0",
+ "contentHash": "GGYLfzV/G/ct80OZ45JxnWP7NvMX1BCugn/lX7TH5o0lcVaviavsLMTxmFV2AybXWjbi3h6FF1vgZiTK6PXndw=="
+ },
+ "Microsoft.TestPlatform.ObjectModel": {
+ "type": "Transitive",
+ "resolved": "18.3.0",
+ "contentHash": "AEIEX2aWdPO9XbtR96eBaJxmXRD9vaI9uQ1T/JbPEKlTAZwYx0ZrMzKyULMdh/HH9Sg03kXCoN7LszQ90o6nPQ=="
+ },
+ "Microsoft.TestPlatform.TestHost": {
+ "type": "Transitive",
+ "resolved": "18.3.0",
+ "contentHash": "twmsoelXnp1uWMU3VGip9f0Jr1mZ0PZqgJdF35CIrdYgYrkHIJMV1m8uKyhcdjLdsQDESHAgkR7KhS9i1qpJag==",
+ "dependencies": {
+ "Microsoft.TestPlatform.ObjectModel": "18.3.0",
+ "Newtonsoft.Json": "13.0.3"
+ }
+ },
+ "OpenTelemetry": {
+ "type": "Transitive",
+ "resolved": "1.15.0",
+ "contentHash": "7mS/oZFF8S6xyqGQfMU1btp0nXJQUPWV535Vp/XMLYwRAUv36xQN+U4vufWBF1+z4HnRTOwuFHtUSGnHbyN6FQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Logging.Configuration": "10.0.0",
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.0"
+ }
+ },
+ "OpenTelemetry.Api": {
+ "type": "Transitive",
+ "resolved": "1.15.0",
+ "contentHash": "vk5OGdf6K9kQScCWo3bRjhDWCv6Pqw92IpX4dlARZ8B1WL7/2NGTDtCkkw42eQf7UdwyoHKzVvMH/PtL8d6z7w=="
+ },
+ "OpenTelemetry.Api.ProviderBuilderExtensions": {
+ "type": "Transitive",
+ "resolved": "1.15.0",
+ "contentHash": "OnuSUlRpGvowkOzGFQfy+KZFu0cITfKfh2IYJJiZskxVJiOuexwOOuvfDAgpJdmTzVWAHjYdz2shcHZaJ06UjQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0",
+ "OpenTelemetry.Api": "1.15.0"
+ }
+ },
+ "Pipelines.Sockets.Unofficial": {
+ "type": "Transitive",
+ "resolved": "2.2.8",
+ "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ=="
+ },
+ "Polly": {
+ "type": "Transitive",
+ "resolved": "7.2.4",
+ "contentHash": "bw00Ck5sh6ekduDE3mnCo1ohzuad946uslCDEENu3091+6UKnBuKLo4e+yaNcCzXxOZCXWY2gV4a35+K1d4LDA=="
+ },
+ "Polly.Extensions.Http": {
+ "type": "Transitive",
+ "resolved": "3.0.0",
+ "contentHash": "drrG+hB3pYFY7w1c3BD+lSGYvH2oIclH8GRSehgfyP5kjnFnHKQuuBhuHLv+PWyFuaTDyk/vfRpnxOzd11+J8g==",
+ "dependencies": {
+ "Polly": "7.1.0"
+ }
+ },
+ "StackExchange.Redis": {
+ "type": "Transitive",
+ "resolved": "2.7.27",
+ "contentHash": "Uqc2OQHglqj9/FfGQ6RkKFkZfHySfZlfmbCl+hc+u2I/IqunfelQ7QJi7ZhvAJxUtu80pildVX6NPLdDaUffOw==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "6.0.0",
+ "Pipelines.Sockets.Unofficial": "2.2.8"
+ }
+ },
+ "System.Diagnostics.EventLog": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "wugvy+pBVzjQEnRs9wMTWwoaeNFX3hsaHeVHFDIvJSWXp7wfmNWu3mxAwBIE6pyW+g6+rHa1Of5fTzb0QVqUTA=="
+ },
+ "xunit.abstractions": {
+ "type": "Transitive",
+ "resolved": "2.0.3",
+ "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg=="
+ },
+ "xunit.analyzers": {
+ "type": "Transitive",
+ "resolved": "1.18.0",
+ "contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ=="
+ },
+ "xunit.assert": {
+ "type": "Transitive",
+ "resolved": "2.9.3",
+ "contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA=="
+ },
+ "xunit.core": {
+ "type": "Transitive",
+ "resolved": "2.9.3",
+ "contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==",
+ "dependencies": {
+ "xunit.extensibility.core": "[2.9.3]",
+ "xunit.extensibility.execution": "[2.9.3]"
+ }
+ },
+ "xunit.extensibility.execution": {
+ "type": "Transitive",
+ "resolved": "2.9.3",
+ "contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==",
+ "dependencies": {
+ "xunit.extensibility.core": "[2.9.3]"
+ }
+ },
+ "contracts": {
+ "type": "Project",
+ "dependencies": {
+ "Grpc.AspNetCore": "[2.76.0, )"
+ }
+ },
+ "infrastructure": {
+ "type": "Project",
+ "dependencies": {
+ "Contracts": "[1.0.0, )",
+ "Microsoft.Extensions.Caching.Hybrid": "[10.4.0, )",
+ "Microsoft.Extensions.Caching.StackExchangeRedis": "[10.0.5, )",
+ "Microsoft.Extensions.Http.Polly": "[10.0.5, )",
+ "OpenTelemetry.Exporter.Console": "[1.15.0, )",
+ "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.0, )",
+ "OpenTelemetry.Extensions": "[1.14.0-beta.1, )",
+ "OpenTelemetry.Extensions.Hosting": "[1.15.0, )",
+ "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.1, )",
+ "OpenTelemetry.Instrumentation.GrpcNetClient": "[1.15.0-beta.1, )",
+ "OpenTelemetry.Instrumentation.Http": "[1.15.0, )",
+ "OpenTelemetry.Instrumentation.Process": "[1.15.0-beta.1, )",
+ "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )",
+ "OpenTelemetry.Instrumentation.StackExchangeRedis": "[1.15.0-beta.1, )"
+ }
+ },
+ "webapp": {
+ "type": "Project",
+ "dependencies": {
+ "AspNetCore.HealthChecks.Redis": "[9.0.0, )",
+ "AspNetCore.HealthChecks.UI.Client": "[9.0.0, )",
+ "AspNetCore.HealthChecks.Uris": "[9.0.0, )",
+ "Infrastructure": "[1.0.0, )",
+ "Microsoft.AspNetCore.OpenApi": "[10.0.5, )",
+ "Scalar.AspNetCore": "[2.13.11, )"
+ }
+ },
+ "AspNetCore.HealthChecks.Redis": {
+ "type": "CentralTransitive",
+ "requested": "[9.0.0, )",
+ "resolved": "9.0.0",
+ "contentHash": "yNH0h8GLRbAf+PU5HNVLZ5hNeyq9mDVmRKO9xuZsme/znUYoBJlQvI0gq45gaZNlLncCHkMhR4o90MuT+gxxPw==",
+ "dependencies": {
+ "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11",
+ "StackExchange.Redis": "2.7.4"
+ }
+ },
+ "AspNetCore.HealthChecks.UI.Client": {
+ "type": "CentralTransitive",
+ "requested": "[9.0.0, )",
+ "resolved": "9.0.0",
+ "contentHash": "1Ub3Wvvbz7CMuFNWgLEc9qqQibiMoovDML/WHrwr5J83RPgtI20giCR92s/ipLgu7IIuqw+W/y7WpIeHqAICxg==",
+ "dependencies": {
+ "AspNetCore.HealthChecks.UI.Core": "9.0.0"
+ }
+ },
+ "AspNetCore.HealthChecks.Uris": {
+ "type": "CentralTransitive",
+ "requested": "[9.0.0, )",
+ "resolved": "9.0.0",
+ "contentHash": "XYdNlA437KeF8p9qOpZFyNqAN+c0FXt/JjTvzH/Qans0q0O3pPE8KPnn39ucQQjR/Roum1vLTP3kXiUs8VHyuA==",
+ "dependencies": {
+ "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11",
+ "Microsoft.Extensions.Http": "8.0.0"
+ }
+ },
+ "Grpc.AspNetCore": {
+ "type": "CentralTransitive",
+ "requested": "[2.76.0, )",
+ "resolved": "2.76.0",
+ "contentHash": "LyXMmpN2Ba0TE35SOLSKbGqIYtJuhc1UgiaGfoW1X8KJERV70QI5KGW+ckEY7MrXoFWN/uWo4B70siVhbDmCgQ==",
+ "dependencies": {
+ "Google.Protobuf": "3.31.1",
+ "Grpc.AspNetCore.Server.ClientFactory": "2.76.0",
+ "Grpc.Tools": "2.76.0"
+ }
+ },
+ "Microsoft.AspNetCore.OpenApi": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "vTcxIfOPyfFbYk1g8YcXJfkMnlEWVkSnnjxcZLy60zgwiHMRf2SnZR+9E4HlpwKxgE3yfKMOti8J6WfKuKsw6w==",
+ "dependencies": {
+ "Microsoft.OpenApi": "2.0.0"
+ }
+ },
+ "Microsoft.Extensions.Caching.Hybrid": {
+ "type": "CentralTransitive",
+ "requested": "[10.4.0, )",
+ "resolved": "10.4.0",
+ "contentHash": "4V+aMLQeU/p4VcIWIcvGro0L6HynmL2TrelL04Ce1iotP6T5+kjxuZQvl6P1ObSXIRPCbVXtQSt1NxK0fRIuag==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.4",
+ "Microsoft.Extensions.Caching.Memory": "10.0.4",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.4",
+ "Microsoft.Extensions.Options": "10.0.4"
+ }
+ },
+ "Microsoft.Extensions.Caching.StackExchangeRedis": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "zXb143/TpEKOLQuWGw2CkJgb9F4XXh2XbevMvppzsIHr1/pjML0zjc+vzXcpCV8YUwpW5NIaScZhzFSm621B3Q==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "StackExchange.Redis": "2.7.27"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Http.Polly": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "4VcH+2eVBQw3MtRpo02nrhv/nU54tL/pkcRF0fSSwD+8MoxgE1EjylPKbSIqHEK7iiB17I0iB37Ao8y+q1sV8g==",
+ "dependencies": {
+ "Microsoft.Extensions.Http": "10.0.5",
+ "Polly": "7.2.4",
+ "Polly.Extensions.Http": "3.0.0"
+ }
+ },
+ "Microsoft.Extensions.Logging": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
+ }
+ },
+ "Newtonsoft.Json": {
+ "type": "CentralTransitive",
+ "requested": "[13.0.4, )",
+ "resolved": "13.0.4",
+ "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A=="
+ },
+ "OpenTelemetry.Exporter.Console": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "Jweov3Q70xmy5U8bwab8xd+xAuaFBI4695q/IpH4/dcAwKytyB+WhV5HufmKfXiKZhRbSEo8piG+i1ENEmdFXw==",
+ "dependencies": {
+ "OpenTelemetry": "1.15.0"
+ }
+ },
+ "OpenTelemetry.Exporter.OpenTelemetryProtocol": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "VH8ANc/js9IRvfYt0Q2UaAxNCOWm+IU+vWrtoH7pfx4oWPVdISUt+9uWfBCFMWZg5WzQip5dhslyDjeyZXXfSQ==",
+ "dependencies": {
+ "OpenTelemetry": "1.15.0"
+ }
+ },
+ "OpenTelemetry.Extensions": {
+ "type": "CentralTransitive",
+ "requested": "[1.14.0-beta.1, )",
+ "resolved": "1.14.0-beta.1",
+ "contentHash": "4DxYvBgz3OaCD8DE2lutiMiaEZh/PvWW05ewhAeqS6lKAETultAzfCveDXxMcIkaVwc93PR/Z22/P5zZ2rmHZA==",
+ "dependencies": {
+ "OpenTelemetry": "[1.14.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Extensions.Hosting": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "RixjKyB1pbYGhWdvPto4KJs+exdQknJsnjUO9WszdLles5Vcd0EYzxPNJdwmLjYfP+Jfbr4B5nktM4ZgeHSWtg==",
+ "dependencies": {
+ "Microsoft.Extensions.Hosting.Abstractions": "10.0.0",
+ "OpenTelemetry": "1.15.0"
+ }
+ },
+ "OpenTelemetry.Instrumentation.AspNetCore": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.1, )",
+ "resolved": "1.15.1",
+ "contentHash": "wXaZTu6LHY8xcbRd6ClcrtjHqGVoGYCcArXEZA3iUjUcYSVYwDGyPU0PdkwTfylxv8JeCCVDQhVb0fT7xBJjGA==",
+ "dependencies": {
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.GrpcNetClient": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.0-beta.1, )",
+ "resolved": "1.15.0-beta.1",
+ "contentHash": "SBas5+C4kGUqoy8OPpQis+QIgJ7/aaJl4H3oLzHCJnZLCb8TXZmQL2/r753RXXJUH8oIeLIzdW+EXgujSy+cpQ==",
+ "dependencies": {
+ "OpenTelemetry": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.Http": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "uToc7bUp8IEdb0ny9mKsL6FrrYelINPzxxiSShJgOf4XmQc4Azww6S5RjRj24YhsOn2a1MABOrxfVTZXtDk4Eg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.0",
+ "Microsoft.Extensions.Options": "10.0.0",
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.Process": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.0-beta.1, )",
+ "resolved": "1.15.0-beta.1",
+ "contentHash": "99zSvpwsMpKextd3RkHGY8iRsmw1qg3PjqkjC5hZI0fZG6m+wPsssrX6z9RhwDGcZ8sdGNjoKyLpNOmR154zQg==",
+ "dependencies": {
+ "OpenTelemetry.Api": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.Runtime": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.0, )",
+ "resolved": "1.15.0",
+ "contentHash": "OOvpqR/j2Pb6+tWhHNODIbSJ53Or/MDtTiXEyrsWI02K2lLAgvBFcxUOrHggS/8015cYR3AdSaXv6NZrkz5yQA==",
+ "dependencies": {
+ "OpenTelemetry.Api": "[1.15.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.StackExchangeRedis": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.0-beta.1, )",
+ "resolved": "1.15.0-beta.1",
+ "contentHash": "Igg/3MlBZZ9lZCTzMcvoFKav263+zOcKx9s4LVIdq96YmBHCuPmDiyygAIPdeIVzwN08VwD3RG1nXHDuRF1Ssg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.0",
+ "Microsoft.Extensions.Options": "10.0.0",
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)",
+ "StackExchange.Redis": "2.6.122"
+ }
+ },
+ "Scalar.AspNetCore": {
+ "type": "CentralTransitive",
+ "requested": "[2.13.11, )",
+ "resolved": "2.13.11",
+ "contentHash": "bH99KIEEaYhC+mMM9011OJtou0y/9O2NXo6h9/k104sAniMzFSGKNaiIX6NRkxc487MJD8vYu3I3nJtW/nU/3g=="
+ },
+ "xunit.extensibility.core": {
+ "type": "CentralTransitive",
+ "requested": "[2.9.3, )",
+ "resolved": "2.9.3",
+ "contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==",
+ "dependencies": {
+ "xunit.abstractions": "2.0.3"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/templates/tests/LoadTests/protos/order.proto b/templates/Bff/tests/LoadTests/protos/order.proto
similarity index 100%
rename from templates/tests/LoadTests/protos/order.proto
rename to templates/Bff/tests/LoadTests/protos/order.proto
diff --git a/templates/tests/LoadTests/scriptGrpc.js b/templates/Bff/tests/LoadTests/scriptGrpc.js
similarity index 89%
rename from templates/tests/LoadTests/scriptGrpc.js
rename to templates/Bff/tests/LoadTests/scriptGrpc.js
index 457d6bcf..1bef07a9 100644
--- a/templates/tests/LoadTests/scriptGrpc.js
+++ b/templates/Bff/tests/LoadTests/scriptGrpc.js
@@ -7,9 +7,9 @@ export const options = {
get_order: {
exec: 'getOrder',
executor: 'constant-vus',
- vus: 10,
- duration: '60s',
- gracefulStop: '10s'
+ vus: __ENV.VUS ? parseInt(__ENV.VUS) : 10,
+ duration: __ENV.DURATION ? __ENV.DURATION : '60s',
+ gracefulStop: __ENV.GRACEFUL_STOP ? __ENV.GRACEFUL_STOP : '10s'
}
},
thresholds: {
diff --git a/templates/tests/LoadTests/scriptHttp.js b/templates/Bff/tests/LoadTests/scriptHttp.js
similarity index 88%
rename from templates/tests/LoadTests/scriptHttp.js
rename to templates/Bff/tests/LoadTests/scriptHttp.js
index f36a1164..5967d804 100644
--- a/templates/tests/LoadTests/scriptHttp.js
+++ b/templates/Bff/tests/LoadTests/scriptHttp.js
@@ -7,9 +7,9 @@ export const options = {
get_order: {
exec: 'getOrder',
executor: 'constant-vus',
- vus: 10,
- duration: '60s',
- gracefulStop: '10s'
+ vus: __ENV.VUS ? parseInt(__ENV.VUS) : 10,
+ duration: __ENV.DURATION ? __ENV.DURATION : '60s',
+ gracefulStop: __ENV.GRACEFUL_STOP ? __ENV.GRACEFUL_STOP : '10s'
}
},
thresholds: {
diff --git a/templates/Full/.editorconfig b/templates/Full/.editorconfig
new file mode 100644
index 00000000..1a25dad5
--- /dev/null
+++ b/templates/Full/.editorconfig
@@ -0,0 +1,199 @@
+# editorconfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Default settings:
+# A newline ending every file
+# Use 4 spaces as indentation
+[*]
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+# Generated code
+[*{_AssemblyInfo.cs,.notsupported.cs,AsmOffsets.cs}]
+generated_code = true
+
+# C# files
+[*.cs]
+# New line preferences
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = one_less_than_current
+
+# Modifier preferences
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
+
+# avoid this. unless absolutely necessary
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_event = false:suggestion
+
+# Types: use keywords instead of BCL types, and permit var only when the type is clear
+csharp_style_var_for_built_in_types = false:suggestion
+csharp_style_var_when_type_is_apparent = false:none
+csharp_style_var_elsewhere = true:suggestion
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+# name all constant fields using PascalCase
+dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
+dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
+dotnet_naming_symbols.constant_fields.applicable_kinds = field
+dotnet_naming_symbols.constant_fields.required_modifiers = const
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+
+# static fields should have s_ prefix
+dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion
+dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields
+dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style
+dotnet_naming_symbols.static_fields.applicable_kinds = field
+dotnet_naming_symbols.static_fields.required_modifiers = static
+dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected
+dotnet_naming_style.static_prefix_style.required_prefix = s_
+dotnet_naming_style.static_prefix_style.capitalization = camel_case
+
+# internal and private fields should be _camelCase
+dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
+dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
+dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
+dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
+dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
+dotnet_naming_style.camel_case_underscore_style.required_prefix = _
+dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
+
+# Code style defaults
+csharp_using_directive_placement = outside_namespace:suggestion
+dotnet_sort_system_directives_first = true
+csharp_preserve_single_line_blocks = true:none
+csharp_preserve_single_line_statements = false:none
+csharp_prefer_static_local_function = true:suggestion
+csharp_prefer_simple_using_statement = false:none
+csharp_style_prefer_switch_expression = true:suggestion
+csharp_style_namespace_declarations = file_scoped:warning
+dotnet_style_readonly_field = true:suggestion
+
+# Expression-level preferences
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_auto_properties = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+csharp_prefer_simple_default_expression = true:suggestion
+
+# Expression-bodied members
+csharp_style_expression_bodied_methods = true:suggestion
+csharp_style_expression_bodied_constructors = true:silent
+csharp_style_expression_bodied_operators = true:silent
+csharp_style_expression_bodied_properties = true:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_lambdas = true:silent
+csharp_style_expression_bodied_local_functions = true:silent
+
+# Pattern matching
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+
+# Null checking preferences
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Other features
+csharp_style_prefer_index_operator = false:none
+csharp_style_prefer_range_operator = false:none
+csharp_style_pattern_local_over_anonymous_function = false:none
+
+# Space preferences
+csharp_space_after_cast = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = do_not_ignore
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# S3216: "ConfigureAwait(false)" should be used
+dotnet_diagnostic.S3216.severity = none
+
+# S2360: Optional parameters should not be used
+dotnet_diagnostic.S2360.severity = none
+
+# CS8618: Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+dotnet_diagnostic.CS8618.severity = none
+
+# CA1873: Avoid potentially expensive logging
+dotnet_diagnostic.CA1873.severity = none
+
+# IDE0005: Using directive is unnecessary
+dotnet_diagnostic.IDE0005.severity = error
+
+# Xml project files
+[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
+indent_size = 2
+
+[*.{csproj,vbproj,proj,nativeproj,locproj}]
+charset = utf-8
+
+# Xml build files
+[*.builds]
+indent_size = 2
+
+# Xml files
+[*.{xml,stylecop,resx,ruleset}]
+indent_size = 2
+
+# Xml config files
+[*.{props,targets,config,nuspec}]
+indent_size = 2
+
+# YAML config files
+[*.{yml,yaml}]
+indent_size = 2
+
+# Shell scripts
+[*.sh]
+end_of_line = lf
+[*.{cmd,bat}]
+end_of_line = crlf
+
+[*.{sql,json}]
+insert_final_newline = false
diff --git a/templates/.github/prompts/create-migrations.prompt.md b/templates/Full/.github/prompts/create-migrations.prompt.md
similarity index 100%
rename from templates/.github/prompts/create-migrations.prompt.md
rename to templates/Full/.github/prompts/create-migrations.prompt.md
diff --git a/templates/.github/prompts/crud-generator.prompt.md b/templates/Full/.github/prompts/crud-generator.prompt.md
similarity index 100%
rename from templates/.github/prompts/crud-generator.prompt.md
rename to templates/Full/.github/prompts/crud-generator.prompt.md
diff --git a/templates/Full/.gitignore b/templates/Full/.gitignore
new file mode 100644
index 00000000..ecced320
--- /dev/null
+++ b/templates/Full/.gitignore
@@ -0,0 +1,482 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from `dotnet new gitignore`
+
+# dotenv files
+.env
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# Tye
+.tye/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+.idea
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# Mac bundle stuff
+*.dmg
+*.app
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# Vim temporary swap files
+*.swp
+
+!Logs
\ No newline at end of file
diff --git a/templates/.template.config/template.json b/templates/Full/.template.config/template.json
similarity index 68%
rename from templates/.template.config/template.json
rename to templates/Full/.template.config/template.json
index 0415d7fe..a298e2f0 100644
--- a/templates/.template.config/template.json
+++ b/templates/Full/.template.config/template.json
@@ -1,11 +1,11 @@
{
"$schema": "http://json.schemastore.org/template",
"author": "Giovanni Brunno Previatti",
- "identity": "Hexagonal.Solution.Template",
- "name": "Hexagonal solution template",
+ "identity": "Hexagonal.Solution.Template.Full",
+ "name": "Hexagonal solution template Full",
"description": "Solution template following hexagonal architecture structure best practices",
- "shortName": "hexagonal-solution",
- "sourceName": "Hexagonal.Solution.Template",
+ "shortName": "full",
+ "sourceName": "Hexagonal.Solution.Template.Full",
"classifications": [
"common",
"template",
diff --git a/templates/Full/.vscode/launch.json b/templates/Full/.vscode/launch.json
new file mode 100644
index 00000000..1243368b
--- /dev/null
+++ b/templates/Full/.vscode/launch.json
@@ -0,0 +1,18 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "WebApp [Development]",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ "program": "${workspaceFolder}/src/WebApp/bin/Debug/net10.0/WebApp.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}/src/WebApp",
+ "stopAtEntry": false,
+ "env": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/templates/Full/.vscode/tasks.json b/templates/Full/.vscode/tasks.json
new file mode 100644
index 00000000..6407faab
--- /dev/null
+++ b/templates/Full/.vscode/tasks.json
@@ -0,0 +1,29 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "${workspaceFolder}/src/WebApp/WebApp.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary;ForceNoAlign"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "watch",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "watch",
+ "run",
+ "--project",
+ "${workspaceFolder}/src/WebApp/WebApp.csproj"
+ ],
+ "problemMatcher": "$msCompile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/templates/Full/Directory.Build.props b/templates/Full/Directory.Build.props
new file mode 100644
index 00000000..5053742d
--- /dev/null
+++ b/templates/Full/Directory.Build.props
@@ -0,0 +1,12 @@
+
+
+ net10.0
+ enable
+ enable
+ true
+ latest
+ Recommended
+ true
+ false
+
+
\ No newline at end of file
diff --git a/templates/Directory.Packages.props b/templates/Full/Directory.Packages.props
similarity index 66%
rename from templates/Directory.Packages.props
rename to templates/Full/Directory.Packages.props
index b9b9e9d1..e621105c 100644
--- a/templates/Directory.Packages.props
+++ b/templates/Full/Directory.Packages.props
@@ -5,37 +5,44 @@
$(NoWarn);NU1507
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/templates/Dockerfile b/templates/Full/Dockerfile
similarity index 100%
rename from templates/Dockerfile
rename to templates/Full/Dockerfile
diff --git a/templates/Full/Hexagonal.Solution.Template.Full.slnx b/templates/Full/Hexagonal.Solution.Template.Full.slnx
new file mode 100644
index 00000000..d86e4772
--- /dev/null
+++ b/templates/Full/Hexagonal.Solution.Template.Full.slnx
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/templates/Readme.md b/templates/Full/Readme.md
similarity index 83%
rename from templates/Readme.md
rename to templates/Full/Readme.md
index 87301605..c636c4c5 100644
--- a/templates/Readme.md
+++ b/templates/Full/Readme.md
@@ -21,13 +21,19 @@ This repository provides a template for building applications using the Hexagona
### Run migrations
```bash
-dotnet ef database update --project src/Infrastructure --startup-project src/WebApp --output-dir src/Infrastructure/Data/Migrations
+dotnet ef database update --project src/Infrastructure --startup-project src/Infrastructure --output-dir src/Infrastructure/Data/Migrations
```
### Create a new migration
```bash
-dotnet ef migrations add --project src/Infrastructure --startup-project src/WebApp --output-dir src/Infrastructure/Data/Migrations
+dotnet ef migrations add --project src/Infrastructure --startup-project src/Infrastructure --output-dir Data/Migrations
+```
+
+### Generate SQL script for migrations with idempotent option
+
+```bash
+dotnet ef migrations script --idempotent --project src/Infrastructure --startup-project src/Infrastructure --output scripts/sql/migrations.sql
```
### Run the application
diff --git a/templates/docker-compose-load-tests.yml b/templates/Full/docker-compose-load-tests.yml
similarity index 58%
rename from templates/docker-compose-load-tests.yml
rename to templates/Full/docker-compose-load-tests.yml
index 4fecc816..b9aa3db5 100644
--- a/templates/docker-compose-load-tests.yml
+++ b/templates/Full/docker-compose-load-tests.yml
@@ -1,30 +1,36 @@
-name: hexagonal-solution-template-load-tests
+name: hexagonal-solution-template-full-load-tests
services:
k6:
+ container_name: k6
image: grafana/k6:1.4.2
depends_on:
webapp:
condition: service_healthy
ports:
- "6565:6565"
+ - "5665:5665"
environment:
- WEBAPP_URL=http://webapp:5000
- K6_SUMMARY_MODE=full
+ - K6_WEB_DASHBOARD=true
+ - K6_WEB_DASHBOARD_EXPORT=html-report.html
+ - VUS=50
+ - DURATION=180s
+ - GRACEFUL_STOP=10s
command: ["run", "/LoadTests/scriptHttp.js", ]
volumes:
- ./tests/LoadTests:/LoadTests
- networks:
- - hexagonal_solution_template_network
webapp:
+ container_name: webapp
build:
context: .
dockerfile: ./Dockerfile
ports:
- "5000:5000"
depends_on:
- sqlserver:
+ postgres:
condition: service_healthy
redis:
condition: service_healthy
@@ -33,44 +39,36 @@ services:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health" ]
interval: 30s
- timeout: 10s
+ timeout: 15s
retries: 5
- start_period: 15s
+ start_period: 30s
environment:
- ASPNETCORE_ENVIRONMENT=Development
- - LOGGING__LOGLEVEL__DEFAULT=Warning
- - LOGGING__LOGLEVEL__MICROSOFT=Warning
- - LOGGING__LOGLEVEL__MICROSOFT_HOSTING_LIFETIME=Warning
- LOGGING__LOGLEVEL__MICROSOFT_ENTITY_FRAMEWORK_CORE=Warning
- LOGGING__LOGLEVEL__MICROSOFT_ENTITY_FRAMEWORK_CORE_DATABASE_COMMAND=Warning
- ENABLE_SENSITIVE_DATA_LOGGING=false
- OTEL_SERVICE_NAME=Hexagonal.Solution.Template.WebApp
- - OTEL_EXPORTER_OTLP_ENDPOINT=http://aspire-dashboard:18889
- - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://aspire-dashboard:18889
- - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://aspire-dashboard:18889
- - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://aspire-dashboard:18889
+ - OTEL_EXPORTER_OTLP_ENDPOINT=http://alloy:4317
- OTEL_EXPORTER_OTLP_PROTOCOL=grpc
- OTEL_RESOURCE_ATTRIBUTES=service.namespace=load-tests
- - ConnectionStrings__OrderDb=Server=sqlserver,1433;Database=OrderDb;User Id=sa;Password=cY5VvZkkh4AzES;TrustServerCertificate=true;
+ - ConnectionStrings__OrderDb=Host=postgres;Port=5432;Database=OrderDb;Username=postgres;Password=cY5VvZkkh4AzES
- ConnectionStrings__Redis=redis:6379
- ConnectionStrings__RabbitMQ=amqp://guest:guest@rabbitmq:5672/
- networks:
- - hexagonal_solution_template_network
- sqlserver:
+ postgres:
extends:
file: docker-compose.yml
- service: sqlserver
+ service: postgres
- mssqltools:
+ postgres-init:
extends:
file: docker-compose.yml
- service: mssqltools
-
- aspire-dashboard:
+ service: postgres-init
+
+ pgadmin:
extends:
file: docker-compose.yml
- service: aspire-dashboard
+ service: pgadmin
redis:
extends:
@@ -82,6 +80,32 @@ services:
file: docker-compose.yml
service: rabbitmq
-networks:
- hexagonal_solution_template_network:
- driver: bridge
+ alloy:
+ extends:
+ file: docker-compose.yml
+ service: alloy
+
+ prometheus:
+ extends:
+ file: docker-compose.yml
+ service: prometheus
+
+ loki:
+ extends:
+ file: docker-compose.yml
+ service: loki
+
+ grafana:
+ extends:
+ file: docker-compose.yml
+ service: grafana
+
+ tempo-init:
+ extends:
+ file: docker-compose.yml
+ service: tempo-init
+
+ tempo:
+ extends:
+ file: docker-compose.yml
+ service: tempo
diff --git a/templates/Full/docker-compose-local.yml b/templates/Full/docker-compose-local.yml
new file mode 100644
index 00000000..a9b94299
--- /dev/null
+++ b/templates/Full/docker-compose-local.yml
@@ -0,0 +1,149 @@
+name: hexagonal-solution-template-full
+
+services:
+ postgres:
+ image: &postgresImage postgres:17-alpine
+ container_name: postgres
+ environment:
+ POSTGRES_PASSWORD: "cY5VvZkkh4AzES"
+ POSTGRES_USER: "postgres"
+ POSTGRES_DB: "OrderDb"
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
+ interval: 3s
+ timeout: 10s
+ retries: 10
+ start_period: 10s
+ ports:
+ - "5432:5432"
+
+ postgres-init:
+ image: *postgresImage
+ container_name: postgres-init
+ depends_on:
+ postgres:
+ condition: service_healthy
+ volumes:
+ - ./scripts/sql/migrations.sql:/tmp/migrations.sql
+ - ./scripts/sql/seeds.sql:/tmp/seeds.sql
+ command:
+ - /bin/sh
+ - -c
+ - |
+ psql -h postgres -U postgres -d OrderDb -f /tmp/migrations.sql &&
+ psql -h postgres -U postgres -d OrderDb -f /tmp/seeds.sql
+ environment:
+ PGPASSWORD: "cY5VvZkkh4AzES"
+
+ pgadmin:
+ image: dpage/pgadmin4:9.12.0
+ container_name: pgadmin
+ environment:
+ PGADMIN_DEFAULT_EMAIL: "admin@admin.com"
+ PGADMIN_DEFAULT_PASSWORD: "admin"
+ depends_on:
+ postgres:
+ condition: service_healthy
+ ports:
+ - "5050:80"
+
+ redis:
+ image: redis:8
+ container_name: redis
+ ports:
+ - "6379:6379"
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 3s
+ timeout: 10s
+ retries: 5
+ start_period: 10s
+
+ rabbitmq:
+ image: rabbitmq:management
+ container_name: rabbitmq
+ ports:
+ - "5672:5672"
+ - "15672:15672"
+ - "15692:15692"
+ environment:
+ RABBITMQ_DEFAULT_USER: guest
+ RABBITMQ_DEFAULT_PASS: guest
+ command: >
+ bash -c "rabbitmq-plugins enable rabbitmq_prometheus &&
+ docker-entrypoint.sh rabbitmq-server"
+ healthcheck:
+ test: ["CMD", "rabbitmq-diagnostics", "ping"]
+ interval: 3s
+ timeout: 10s
+ retries: 5
+ start_period: 10s
+
+ alloy:
+ container_name: alloy
+ image: grafana/alloy:v1.7.5
+ ports:
+ - 12345:12345
+ - 4318:4318
+ - 4317:4317
+ volumes:
+ - ./scripts/grafana/config.alloy:/etc/alloy/config.alloy
+ - /var/lib/docker/containers:/var/lib/docker/containers:ro
+ - /var/run/docker.sock:/var/run/docker.sock
+ command: run --server.http.listen-addr=0.0.0.0:12345 --storage.path=/var/lib/alloy/data /etc/alloy/config.alloy
+ depends_on:
+ - loki
+ - prometheus
+ - tempo
+
+ prometheus:
+ container_name: prometheus
+ image: prom/prometheus:v3.1.0
+ ports:
+ - 9090:9090
+ volumes:
+ - ./scripts/grafana/prometheus.yml:/etc/prometheus/prometheus.yml
+ command: --config.file=/etc/prometheus/prometheus.yml --web.enable-otlp-receiver --enable-feature=exemplar-storage
+
+ loki:
+ container_name: loki
+ image: grafana/loki:3.4.2
+ ports:
+ - "3100:3100"
+ volumes:
+ - ./scripts/grafana/loki-config.yaml:/etc/loki/local-config.yaml
+ command: -config.file=/etc/loki/local-config.yaml
+
+ grafana:
+ container_name: grafana
+ image: grafana/grafana:11.5.2
+ environment:
+ - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
+ - GF_AUTH_ANONYMOUS_ENABLED=true
+ - GF_AUTH_BASIC_ENABLED=false
+ - GF_FEATURE_TOGGLES_ENABLE=accessControlOnCall
+ ports:
+ - 3000:3000/tcp
+ volumes:
+ - ./scripts/grafana/datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
+
+ tempo-init:
+ container_name: tempo-init
+ image: &tempoImage grafana/tempo:2.7.1
+ user: root
+ entrypoint:
+ - "chown"
+ - "10001:10001"
+ - "/var/tempo"
+
+ tempo:
+ container_name: tempo
+ image: *tempoImage
+ command: [ "-config.file=/etc/tempo.yaml" ]
+ volumes:
+ - ./scripts/grafana/tempo.yaml:/etc/tempo.yaml
+ ports:
+ - "3200:3200" # tempo
+ depends_on:
+ - tempo-init
+ - redis
\ No newline at end of file
diff --git a/templates/Full/docker-compose.yml b/templates/Full/docker-compose.yml
new file mode 100644
index 00000000..f421cf90
--- /dev/null
+++ b/templates/Full/docker-compose.yml
@@ -0,0 +1,68 @@
+name: hexagonal-solution-template-full
+
+services:
+ postgres:
+ image: &postgresImage postgres:17-alpine
+ container_name: postgres
+ environment:
+ POSTGRES_PASSWORD: "cY5VvZkkh4AzES"
+ POSTGRES_USER: "postgres"
+ POSTGRES_DB: "OrderDb"
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
+ interval: 3s
+ timeout: 10s
+ retries: 10
+ start_period: 10s
+ ports:
+ - "5432:5432"
+
+ postgres-init:
+ image: *postgresImage
+ container_name: postgres-init
+ depends_on:
+ postgres:
+ condition: service_healthy
+ volumes:
+ - ./scripts/sql/migrations.sql:/tmp/migrations.sql
+ - ./scripts/sql/seeds.sql:/tmp/seeds.sql
+ command:
+ - /bin/sh
+ - -c
+ - |
+ psql -h postgres -U postgres -d OrderDb -f /tmp/migrations.sql &&
+ psql -h postgres -U postgres -d OrderDb -f /tmp/seeds.sql
+ environment:
+ PGPASSWORD: "cY5VvZkkh4AzES"
+
+ redis:
+ image: redis:8
+ container_name: redis
+ ports:
+ - "6379:6379"
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 3s
+ timeout: 10s
+ retries: 5
+ start_period: 10s
+
+ rabbitmq:
+ image: rabbitmq:management
+ container_name: rabbitmq
+ ports:
+ - "5672:5672"
+ - "15672:15672"
+ - "15692:15692"
+ environment:
+ RABBITMQ_DEFAULT_USER: guest
+ RABBITMQ_DEFAULT_PASS: guest
+ command: >
+ bash -c "rabbitmq-plugins enable rabbitmq_prometheus &&
+ docker-entrypoint.sh rabbitmq-server"
+ healthcheck:
+ test: ["CMD", "rabbitmq-diagnostics", "ping"]
+ interval: 3s
+ timeout: 10s
+ retries: 5
+ start_period: 10s
\ No newline at end of file
diff --git a/templates/Full/scripts/grafana/config.alloy b/templates/Full/scripts/grafana/config.alloy
new file mode 100644
index 00000000..34d692e5
--- /dev/null
+++ b/templates/Full/scripts/grafana/config.alloy
@@ -0,0 +1,45 @@
+otelcol.receiver.otlp "default" {
+ http {
+ endpoint = "0.0.0.0:4318"
+
+ }
+ grpc {
+ endpoint = "0.0.0.0:4317"
+ }
+
+ output {
+ metrics = [otelcol.exporter.otlphttp.metrics.input]
+ logs = [otelcol.exporter.otlphttp.logs.input]
+ traces = [otelcol.exporter.otlphttp.traces.input,otelcol.connector.servicegraph.default.input]
+ }
+}
+
+otelcol.connector.servicegraph "default" {
+ dimensions = ["http.method"]
+
+ output {
+ metrics = [otelcol.exporter.otlphttp.metrics.input]
+ }
+}
+
+otelcol.exporter.otlphttp "logs" {
+ client {
+ endpoint = "http://loki:3100/otlp"
+ }
+}
+
+otelcol.exporter.otlphttp "metrics" {
+ client {
+ endpoint = "http://prometheus:9090/api/v1/otlp"
+ }
+}
+
+otelcol.exporter.otlphttp "traces" {
+ client {
+ endpoint = "http://tempo:4318"
+ }
+}
+
+livedebugging {
+ enabled = true
+}
\ No newline at end of file
diff --git a/templates/Full/scripts/grafana/datasources.yaml b/templates/Full/scripts/grafana/datasources.yaml
new file mode 100644
index 00000000..b6e00255
--- /dev/null
+++ b/templates/Full/scripts/grafana/datasources.yaml
@@ -0,0 +1,55 @@
+apiVersion: 1
+
+datasources:
+- name: Tempo
+ type: tempo
+ access: proxy
+ orgId: 1
+ url: http://tempo:3200
+ basicAuth: false
+ isDefault: true
+ version: 1
+ editable: false
+ apiVersion: 1
+ uid: tempo
+ jsonData:
+ httpMethod: GET
+ serviceMap:
+ datasourceUid: prometheus
+ tracesToLogsV2:
+ # Field with an internal link pointing to a logs data source in Grafana.
+ # datasourceUid value must match the uid value of the logs data source.
+ datasourceUid: 'Loki'
+ spanStartTimeShift: '-1h'
+ spanEndTimeShift: '1h'
+ filterByTraceID: true
+ filterBySpanID: true
+ customQuery: false
+ query: 'method="$${__span.tags.method}"'
+- name: Prometheus
+ type: prometheus
+ uid: prometheus
+ access: proxy
+ orgId: 1
+ url: http://prometheus:9090
+ basicAuth: false
+ isDefault: false
+ version: 1
+ editable: false
+ jsonData:
+ httpMethod: GET
+- name: Loki
+ type: loki
+ access: proxy
+ orgId: 1
+ url: http://loki:3100
+ basicAuth: false
+ isDefault: false
+ version: 1
+ editable: false
+ jsonData:
+ derivedFields:
+ - datasourceUid: tempo
+ matcherRegex: tid=(\w+)
+ name: TraceId
+ url: $${__value.raw}
diff --git a/templates/Full/scripts/grafana/loki-config.yaml b/templates/Full/scripts/grafana/loki-config.yaml
new file mode 100644
index 00000000..0efbec82
--- /dev/null
+++ b/templates/Full/scripts/grafana/loki-config.yaml
@@ -0,0 +1,39 @@
+# This is a complete configuration to deploy Loki backed by the filesystem.
+# The index will be shipped to the storage via tsdb-shipper.
+
+auth_enabled: false
+
+limits_config:
+ allow_structured_metadata: true
+ volume_enabled: true
+
+server:
+ http_listen_port: 3100
+
+common:
+ ring:
+ instance_addr: 0.0.0.0
+ kvstore:
+ store: inmemory
+ replication_factor: 1
+ path_prefix: /tmp/loki
+
+schema_config:
+ configs:
+ - from: 2020-05-15
+ store: tsdb
+ object_store: filesystem
+ schema: v13
+ index:
+ prefix: index_
+ period: 24h
+
+storage_config:
+ tsdb_shipper:
+ active_index_directory: /tmp/loki/index
+ cache_location: /tmp/loki/index_cache
+ filesystem:
+ directory: /tmp/loki/chunks
+
+pattern_ingester:
+ enabled: true
\ No newline at end of file
diff --git a/templates/Full/scripts/grafana/prometheus.yml b/templates/Full/scripts/grafana/prometheus.yml
new file mode 100644
index 00000000..e71c1eb3
--- /dev/null
+++ b/templates/Full/scripts/grafana/prometheus.yml
@@ -0,0 +1,7 @@
+global:
+ scrape_interval: 3s
+ evaluation_interval: 3s
+
+storage:
+ tsdb:
+ out_of_order_time_window: 30m
diff --git a/templates/Full/scripts/grafana/tempo.yaml b/templates/Full/scripts/grafana/tempo.yaml
new file mode 100644
index 00000000..94b3d42f
--- /dev/null
+++ b/templates/Full/scripts/grafana/tempo.yaml
@@ -0,0 +1,89 @@
+stream_over_http_enabled: true
+server:
+ http_listen_port: 3200
+ log_level: info
+
+cache:
+ background:
+ writeback_goroutines: 5
+ caches:
+ - roles:
+ - frontend-search
+ redis:
+ endpoint: redis:6379
+
+query_frontend:
+ search:
+ duration_slo: 5s
+ throughput_bytes_slo: 1.073741824e+09
+ metadata_slo:
+ duration_slo: 5s
+ throughput_bytes_slo: 1.073741824e+09
+ trace_by_id:
+ duration_slo: 100ms
+ metrics:
+ max_duration: 120h # maximum duration of a metrics query, increase for local setups
+ query_backend_after: 5m
+ duration_slo: 5s
+ throughput_bytes_slo: 1.073741824e+09
+
+distributor:
+ receivers: # this configuration will listen on all ports and protocols that tempo is capable of.
+ jaeger: # the receives all come from the OpenTelemetry collector. more configuration information can
+ protocols: # be found there: https://github.com/open-telemetry/opentelemetry-collector/tree/main/receiver
+ thrift_http: #
+ endpoint: "tempo:14268" # for a production deployment you should only enable the receivers you need!
+ grpc:
+ endpoint: "tempo:14250"
+ thrift_binary:
+ endpoint: "tempo:6832"
+ thrift_compact:
+ endpoint: "tempo:6831"
+ zipkin:
+ endpoint: "tempo:9411"
+ otlp:
+ protocols:
+ grpc:
+ endpoint: "tempo:4317"
+ http:
+ endpoint: "tempo:4318"
+ opencensus:
+ endpoint: "tempo:55678"
+
+ingester:
+ max_block_duration: 5m # cut the headblock when this much time passes. this is being set for demo purposes and should probably be left alone normally
+
+compactor:
+ compaction:
+ block_retention: 24h # overall Tempo trace retention. set for demo purposes
+
+metrics_generator:
+ registry:
+ external_labels:
+ source: tempo
+ cluster: docker-compose
+ storage:
+ path: /var/tempo/generator/wal
+ remote_write:
+ - url: http://prometheus:9090/api/v1/write
+ send_exemplars: true
+ traces_storage:
+ path: /var/tempo/generator/traces
+ processor:
+ local_blocks:
+ filter_server_spans: false
+ flush_to_storage: true
+
+storage:
+ trace:
+ backend: local # backend configuration to use
+ wal:
+ path: /var/tempo/wal # where to store the wal locally
+ local:
+ path: /var/tempo/blocks
+
+overrides:
+ defaults:
+ metrics_generator:
+ processors: [service-graphs, span-metrics, local-blocks] # enables metrics generator
+ generate_native_histograms: both
\ No newline at end of file
diff --git a/templates/Full/scripts/sql/migrations.sql b/templates/Full/scripts/sql/migrations.sql
new file mode 100644
index 00000000..efbb37b3
--- /dev/null
+++ b/templates/Full/scripts/sql/migrations.sql
@@ -0,0 +1,82 @@
+CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
+ "MigrationId" character varying(150) NOT NULL,
+ "ProductVersion" character varying(32) NOT NULL,
+ CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
+);
+
+START TRANSACTION;
+
+DO $EF$
+BEGIN
+ IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260403110832_CreateTables') THEN
+ CREATE TABLE "Notification" (
+ "Id" integer GENERATED BY DEFAULT AS IDENTITY,
+ "NotificationType" integer NOT NULL,
+ "NotificationStatus" text NOT NULL,
+ "Message" text,
+ "CreatedAt" timestamp with time zone NOT NULL,
+ "CreatedBy" text,
+ "CreatedByTimezoneId" text NOT NULL,
+ "UpdatedAt" timestamp with time zone NOT NULL,
+ "UpdatedBy" text,
+ "UpdatedByTimezoneId" text,
+ CONSTRAINT "PK_Notification" PRIMARY KEY ("Id")
+ );
+ END IF;
+END $EF$;
+
+DO $EF$
+BEGIN
+ IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260403110832_CreateTables') THEN
+ CREATE TABLE "Order" (
+ "Id" integer GENERATED BY DEFAULT AS IDENTITY,
+ "Description" text NOT NULL,
+ "Total" numeric(18,2) NOT NULL,
+ "CreatedAt" timestamp with time zone NOT NULL,
+ "CreatedBy" text,
+ "CreatedByTimezoneId" text NOT NULL,
+ "UpdatedAt" timestamp with time zone NOT NULL,
+ "UpdatedBy" text,
+ "UpdatedByTimezoneId" text,
+ CONSTRAINT "PK_Order" PRIMARY KEY ("Id")
+ );
+ END IF;
+END $EF$;
+
+DO $EF$
+BEGIN
+ IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260403110832_CreateTables') THEN
+ CREATE TABLE "Item" (
+ "Id" integer GENERATED BY DEFAULT AS IDENTITY,
+ "Name" text NOT NULL,
+ "Description" text NOT NULL,
+ "Value" numeric(18,2) NOT NULL,
+ "OrderId" integer,
+ "CreatedAt" timestamp with time zone NOT NULL,
+ "CreatedBy" text,
+ "CreatedByTimezoneId" text NOT NULL,
+ "UpdatedAt" timestamp with time zone NOT NULL,
+ "UpdatedBy" text,
+ "UpdatedByTimezoneId" text,
+ CONSTRAINT "PK_Item" PRIMARY KEY ("Id"),
+ CONSTRAINT "FK_Item_Order_OrderId" FOREIGN KEY ("OrderId") REFERENCES "Order" ("Id")
+ );
+ END IF;
+END $EF$;
+
+DO $EF$
+BEGIN
+ IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260403110832_CreateTables') THEN
+ CREATE INDEX "IX_Item_OrderId" ON "Item" ("OrderId");
+ END IF;
+END $EF$;
+
+DO $EF$
+BEGIN
+ IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260403110832_CreateTables') THEN
+ INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
+ VALUES ('20260403110832_CreateTables', '10.0.5');
+ END IF;
+END $EF$;
+COMMIT;
+
diff --git a/templates/Full/scripts/sql/seeds.sql b/templates/Full/scripts/sql/seeds.sql
new file mode 100644
index 00000000..753a3098
--- /dev/null
+++ b/templates/Full/scripts/sql/seeds.sql
@@ -0,0 +1,31 @@
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM "Order" WHERE "Id" = 1 LIMIT 1) THEN
+ INSERT INTO "Order" ("Id", "Description", "Total", "CreatedAt", "UpdatedAt", "CreatedBy", "CreatedByTimezoneId")
+ VALUES (1, 'XPTO Client Computers', 1000.00, NOW(), NOW(), 'System', 'UTC'),
+ (2, 'Contoso', 1200.00, NOW(), NOW(), 'System', 'America/New_York');
+
+ -- Reset the sequence to the maximum ID + 1
+ PERFORM setval(pg_get_serial_sequence('"Order"', 'Id'), COALESCE(MAX("Id"), 1), true) FROM "Order";
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM "Item" WHERE "Id" = 1 LIMIT 1) THEN
+ INSERT INTO "Item" ("Id", "Name", "Description", "Value", "OrderId", "CreatedAt", "UpdatedAt", "CreatedBy", "CreatedByTimezoneId")
+ VALUES (1, 'Graphics Card 4090 Super', 'Nvidia Graphics Cards 24GB RX 4090 Super', 1000.00, 1, NOW(), NOW(), 'System', 'UTC'),
+ (3, 'Notebook', 'Notebook with Intel i7 and 16GB RAM', 1200.00, 2, NOW(), NOW(), 'System', 'America/New_York');
+
+ -- Reset the sequence to the maximum ID + 1
+ PERFORM setval(pg_get_serial_sequence('"Item"', 'Id'), COALESCE(MAX("Id"), 1), true) FROM "Item";
+ END IF;
+END $$;
+
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM "Notification" WHERE "Id" = 1 LIMIT 1) THEN
+ INSERT INTO "Notification" ("Id", "NotificationType", "NotificationStatus", "CreatedAt", "UpdatedAt", "CreatedBy", "CreatedByTimezoneId")
+ VALUES (1, 1, 'Created', NOW(), NOW(), 'System', 'UTC');
+
+ -- Reset the sequence to the maximum ID + 1
+ PERFORM setval(pg_get_serial_sequence('"Notification"', 'Id'), COALESCE(MAX("Id"), 1), true) FROM "Notification";
+ END IF;
+END $$;
diff --git a/templates/src/Application/Application.csproj b/templates/Full/src/Application/Application.csproj
similarity index 86%
rename from templates/src/Application/Application.csproj
rename to templates/Full/src/Application/Application.csproj
index 9162a973..a042ffcc 100644
--- a/templates/src/Application/Application.csproj
+++ b/templates/Full/src/Application/Application.csproj
@@ -3,6 +3,7 @@
true
+
diff --git a/templates/src/Application/ApplicationDependencyInjection.cs b/templates/Full/src/Application/ApplicationDependencyInjection.cs
similarity index 97%
rename from templates/src/Application/ApplicationDependencyInjection.cs
rename to templates/Full/src/Application/ApplicationDependencyInjection.cs
index be60f15c..9a13eb2a 100644
--- a/templates/src/Application/ApplicationDependencyInjection.cs
+++ b/templates/Full/src/Application/ApplicationDependencyInjection.cs
@@ -22,7 +22,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services
private static void RegisterUseCases(IServiceCollection services, Assembly applicationAssembly)
{
var useCaseTypes = applicationAssembly.GetTypes()
- .Where(t => t.IsClass && !t.IsAbstract && t.Name.EndsWith("UseCase"))
+ .Where(t => t.IsClass && !t.IsAbstract && t.Name.EndsWith("UseCase", StringComparison.Ordinal))
.ToList();
foreach (var useCaseType in useCaseTypes)
diff --git a/templates/Full/src/Application/Common/Helpers/Logs.cs b/templates/Full/src/Application/Common/Helpers/Logs.cs
new file mode 100644
index 00000000..acb0a91d
--- /dev/null
+++ b/templates/Full/src/Application/Common/Helpers/Logs.cs
@@ -0,0 +1,172 @@
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.Logging;
+
+namespace Application.Common.Helpers;
+
+public partial class Logs
+{
+ ///
+ /// Logs a generic debug message with a custom message.
+ ///
+ /// The logger instance to use for logging.
+ /// The correlation ID for tracking the request.
+ /// The debug message.
+ /// The name of the method (auto-captured).
+ [LoggerMessage(
+ EventId = 1,
+ Level = LogLevel.Debug,
+ Message = "[{MethodName}] | [{CorrelationId}] | {Message}"
+ )]
+ public static partial void Debug(ILogger logger, Guid correlationId, string message, [CallerMemberName] string methodName = null!);
+
+ ///
+ /// Logs a generic information message with a custom message.
+ ///
+ /// The logger instance to use for logging.
+ /// The correlation ID for tracking the request.
+ /// The information message.
+ /// The name of the method (auto-captured).
+ [LoggerMessage(
+ EventId = 2,
+ Level = LogLevel.Information,
+ Message = "[{MethodName}] | [{CorrelationId}] | {Message}"
+ )]
+ public static partial void Information(ILogger logger, Guid correlationId, string message, [CallerMemberName] string methodName = null!);
+
+ ///
+ /// Logs a generic warning message with a custom message.
+ ///
+ /// The logger instance to use for logging.
+ /// The correlation ID for tracking the request.
+ /// The warning message.
+ /// The name of the method (auto-captured).
+ [LoggerMessage(
+ EventId = 3,
+ Level = LogLevel.Warning,
+ Message = "[{MethodName}] | [{CorrelationId}] | {Message}"
+ )]
+ public static partial void Warning(ILogger logger, Guid correlationId, string message, [CallerMemberName] string methodName = null!);
+
+ ///
+ /// Logs a generic operation failure with a custom message.
+ ///
+ /// The logger instance to use for logging.
+ /// The failure message.
+ /// The name of the method (auto-captured).
+ [LoggerMessage(
+ EventId = 4,
+ Level = LogLevel.Error,
+ Message = "[{MethodName}] | Error: {Message}"
+ )]
+ public static partial void Error(ILogger logger, string message, [CallerMemberName] string methodName = null!);
+
+ ///
+ /// Logs a generic operation failure with a custom message.
+ ///
+ /// The logger instance to use for logging.
+ /// The correlation ID for tracking the request.
+ /// The failure message.
+ /// The name of the method (auto-captured).
+ [LoggerMessage(
+ EventId = 5,
+ Level = LogLevel.Error,
+ Message = "[{MethodName}] | [{CorrelationId}] | Error: {Message}"
+ )]
+ public static partial void Error(ILogger logger, Guid correlationId, string message, [CallerMemberName] string methodName = null!);
+
+ ///
+ /// Logs the start of the execution of an operation, including the method name and correlation ID.
+ ///
+ /// The logger instance to use for logging.
+ /// The correlation ID for tracking the request.
+ /// The name of the method where the operation is executed (auto-captured).
+ [LoggerMessage(
+ EventId = 6,
+ Level = LogLevel.Information,
+ Message = "[{MethodName}] | [{CorrelationId}] | Starting operation"
+ )]
+ public static partial void StartingOperation(ILogger logger, Guid correlationId, [CallerMemberName] string methodName = null!);
+
+ ///
+ /// Logs the completion of the execution of an operation, including the method name and correlation ID.
+ ///
+ /// The logger instance to use for logging.
+ /// The correlation ID for tracking the request.
+ /// The name of the method where the operation is executed (auto-captured).
+ [LoggerMessage(
+ EventId = 7,
+ Level = LogLevel.Information,
+ Message = "[{MethodName}] | [{CorrelationId}] | Finished operation"
+ )]
+ public static partial void FinishedOperation(ILogger logger, Guid correlationId, [CallerMemberName] string methodName = null!);
+
+ ///
+ /// Logs validation errors encountered during request validation.
+ ///
+ /// The logger instance to use for logging.
+ /// The correlation ID for tracking the request.
+ /// The validation errors.
+ /// The name of the method where the validation occurred (auto-captured).
+ [LoggerMessage(
+ EventId = 8,
+ Level = LogLevel.Error,
+ Message = "[{MethodName}] | [{CorrelationId}] | Validation errors: {Errors}"
+ )]
+ public static partial void ValidationErrors(ILogger logger, Guid correlationId, string errors, [CallerMemberName] string methodName = null!);
+
+ ///
+ /// Logs when an entity is not found.
+ ///
+ /// The logger instance to use for logging.
+ /// The correlation ID for tracking the request.
+ /// The name of the entity that was not found.
+ /// The name of the method (auto-captured).
+ [LoggerMessage(
+ EventId = 9,
+ Level = LogLevel.Warning,
+ Message = "[{MethodName}] | [{CorrelationId}] | {EntityName} not found."
+ )]
+ public static partial void NotFound(ILogger logger, Guid correlationId, string entityName, [CallerMemberName] string methodName = null!);
+
+ ///
+ /// Logs a generic operation failure with a custom message.
+ ///
+ /// The logger instance to use for logging.
+ /// The correlation ID for tracking the request.
+ /// The failure message.
+ /// The name of the method (auto-captured).
+ [LoggerMessage(
+ EventId = 10,
+ Level = LogLevel.Warning,
+ Message = "[{MethodName}] | [{CorrelationId}] | Failed operation: {Message}"
+ )]
+ public static partial void FailedOperation(ILogger logger, Guid correlationId, string message, [CallerMemberName] string methodName = null!);
+
+ ///
+ /// Logs the start of an operation.
+ ///
+ /// The logger instance to use for logging.
+ /// The correlation ID for tracking the request.
+ /// Optional details about the operation.
+ /// The method name (auto-captured).
+ [LoggerMessage(
+ EventId = 11,
+ Level = LogLevel.Debug,
+ Message = "[{Method}] | [{CorrelationId}] | Starting operation. | {Details}"
+ )]
+ public static partial void DebugStartingOperation(ILogger logger, Guid correlationId, string details = "", [CallerMemberName] string method = null!);
+
+ ///
+ /// Logs the completion of an operation.
+ ///
+ /// The logger instance to use for logging.
+ /// The correlation ID for tracking the request.
+ /// Optional details about the operation.
+ /// The method name (auto-captured).
+ [LoggerMessage(
+ EventId = 12,
+ Level = LogLevel.Debug,
+ Message = "[{Method}] | [{CorrelationId}] | Finished operation. | {Details}"
+ )]
+ public static partial void DebugFinishedOperation(ILogger logger, Guid correlationId, string details = "", [CallerMemberName] string method = null!);
+}
\ No newline at end of file
diff --git a/templates/src/Application/Common/Messages/BaseMessage.cs b/templates/Full/src/Application/Common/Messages/BaseMessage.cs
similarity index 100%
rename from templates/src/Application/Common/Messages/BaseMessage.cs
rename to templates/Full/src/Application/Common/Messages/BaseMessage.cs
diff --git a/templates/src/Application/Common/Messages/CreateNotificationMessage.cs b/templates/Full/src/Application/Common/Messages/CreateNotificationMessage.cs
similarity index 78%
rename from templates/src/Application/Common/Messages/CreateNotificationMessage.cs
rename to templates/Full/src/Application/Common/Messages/CreateNotificationMessage.cs
index ac8c5716..2a07f9a0 100644
--- a/templates/src/Application/Common/Messages/CreateNotificationMessage.cs
+++ b/templates/Full/src/Application/Common/Messages/CreateNotificationMessage.cs
@@ -1,8 +1,10 @@
+using Domain.Common.Enums;
+
namespace Application.Common.Messages;
public sealed record CreateNotificationMessage(
Guid CorrelationId,
- string NotificationType,
+ NotificationType NotificationType,
string NotificationStatus,
string? CreatedBy = null,
object? Message = null
diff --git a/templates/Full/src/Application/Common/Repositories/IBaseRepository.cs b/templates/Full/src/Application/Common/Repositories/IBaseRepository.cs
new file mode 100644
index 00000000..4e042aa0
--- /dev/null
+++ b/templates/Full/src/Application/Common/Repositories/IBaseRepository.cs
@@ -0,0 +1,38 @@
+using System.Linq.Expressions;
+using System.Runtime.CompilerServices;
+using Domain.Common;
+
+namespace Application.Common.Repositories;
+public interface IBaseRepository
+{
+ Task AddAsync(TEntity entity, Guid correlationId, CancellationToken cancellationToken, bool? newContext = null) where TEntity : DomainEntity;
+ Task AddRangeAsync(TEntity[] entities, Guid correlationId, CancellationToken cancellationToken, bool? newContext = null) where TEntity : DomainEntity;
+ Task UpdateAsync(TEntity entity, Guid correlationId, CancellationToken cancellationToken, bool? newContext = null) where TEntity : DomainEntity;
+ Task RemoveAsync(TEntity entity, Guid correlationId, CancellationToken cancellationToken, bool? newContext = null) where TEntity : DomainEntity;
+ Task RemoveRangeAsync(TEntity[] entities, Guid correlationId, CancellationToken cancellationToken, bool? newContext = null) where TEntity : DomainEntity;
+ IQueryable GetQueryable(Guid correlationId, bool? newContext = null, [CallerMemberName] string methodName = null!) where TEntity : DomainEntity;
+ Task<(IEnumerable Items, int TotalRecords)> GetAllPaginatedAsync(
+ Guid correlationId,
+ int page,
+ int pageSize,
+ CancellationToken cancellationToken,
+ string? sortBy = null,
+ bool sortDescending = false,
+ Dictionary? searchByValues = null,
+ bool? newContext = null,
+ params Expression>[]? includes
+ ) where TEntity : DomainEntity;
+
+ Task<(IEnumerable Items, int TotalRecords)> GetAllPaginatedAsync(
+ Guid correlationId,
+ int page,
+ int pageSize,
+ Expression> selector,
+ CancellationToken cancellationToken,
+ string? sortBy = null,
+ bool sortDescending = false,
+ Dictionary? searchByValues = null,
+ Expression> predicate = null!,
+ bool? newContext = null
+ ) where TEntity : DomainEntity;
+}
diff --git a/templates/src/Application/Common/Requests/BaseRequest.cs b/templates/Full/src/Application/Common/Requests/BaseRequest.cs
similarity index 62%
rename from templates/src/Application/Common/Requests/BaseRequest.cs
rename to templates/Full/src/Application/Common/Requests/BaseRequest.cs
index 6ccbb7a0..5cf1caa3 100644
--- a/templates/src/Application/Common/Requests/BaseRequest.cs
+++ b/templates/Full/src/Application/Common/Requests/BaseRequest.cs
@@ -1,7 +1,7 @@
using FluentValidation;
namespace Application.Common.Requests;
-public record BaseRequest(Guid CorrelationId);
+public record BaseRequest(Guid CorrelationId, string User = "", string TimezoneId = "");
public record BasePaginatedRequest(
Guid CorrelationId,
@@ -9,8 +9,10 @@ public record BasePaginatedRequest(
int PageSize = 10,
string? SortBy = null,
bool SortDescending = false,
- Dictionary? SearchByValues = null
-) : BaseRequest(CorrelationId);
+ Dictionary? SearchByValues = null,
+ string User = "",
+ string TimezoneId = ""
+) : BaseRequest(CorrelationId, User, TimezoneId);
public sealed class BasePaginatedRequestValidator : AbstractValidator
{
@@ -22,8 +24,14 @@ public BasePaginatedRequestValidator()
RuleFor(r => r.PageSize)
.GreaterThan(0)
+
.WithMessage("PageSize must be greater than 0")
.LessThanOrEqualTo(100)
.WithMessage("PageSize must be less than or equal to 100");
+
+ RuleFor(r => r.TimezoneId)
+ .NotEmpty()
+ .When(r => !string.IsNullOrEmpty(r.User))
+ .WithMessage("TimezoneId is required when User is provided");
}
}
diff --git a/templates/src/Application/Common/Requests/BaseResponse.cs b/templates/Full/src/Application/Common/Requests/BaseResponse.cs
similarity index 100%
rename from templates/src/Application/Common/Requests/BaseResponse.cs
rename to templates/Full/src/Application/Common/Requests/BaseResponse.cs
diff --git a/templates/src/Application/Common/Services/IHybridCacheService.cs b/templates/Full/src/Application/Common/Services/IHybridCacheService.cs
similarity index 72%
rename from templates/src/Application/Common/Services/IHybridCacheService.cs
rename to templates/Full/src/Application/Common/Services/IHybridCacheService.cs
index d68be0c9..6c28823d 100644
--- a/templates/src/Application/Common/Services/IHybridCacheService.cs
+++ b/templates/Full/src/Application/Common/Services/IHybridCacheService.cs
@@ -3,16 +3,18 @@ namespace Application.Common.Services;
public interface IHybridCacheService
{
ValueTask GetOrCreateAsync(
+ Guid correlationId,
string key,
Func> factory,
CancellationToken cancellationToken
);
ValueTask CreateAsync(
+ Guid correlationId,
string key,
TResult value,
CancellationToken cancellationToken
);
- ValueTask DeleteAsync(string key, CancellationToken cancellationToken);
+ ValueTask DeleteAsync(Guid correlationId, string key, CancellationToken cancellationToken);
}
diff --git a/templates/src/Application/Common/Services/IProduceService.cs b/templates/Full/src/Application/Common/Services/IProduceService.cs
similarity index 100%
rename from templates/src/Application/Common/Services/IProduceService.cs
rename to templates/Full/src/Application/Common/Services/IProduceService.cs
diff --git a/templates/Full/src/Application/Common/UseCases/BaseInOutUseCase.cs b/templates/Full/src/Application/Common/UseCases/BaseInOutUseCase.cs
new file mode 100644
index 00000000..2c9e54ac
--- /dev/null
+++ b/templates/Full/src/Application/Common/UseCases/BaseInOutUseCase.cs
@@ -0,0 +1,68 @@
+using Application.Common.Helpers;
+using Application.Common.Repositories;
+using Application.Common.Requests;
+using Application.Common.Services;
+using FluentValidation;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Application.Common.UseCases;
+
+public interface IBaseInOutUseCase
+ where TRequest : BaseRequest
+ where TResponseData : BaseResponse
+{
+ Task HandleAsync(TRequest request, CancellationToken cancellationToken);
+}
+
+public abstract class BaseInOutUseCase(IServiceProvider serviceProvider) : BaseUseCase(serviceProvider), IBaseInOutUseCase
+ where TRequest : BaseRequest
+ where TResponseData : BaseResponse
+{
+ protected IHybridCacheService Cache { get; } = serviceProvider.GetRequiredService();
+ protected IBaseRepository Repository { get; } = serviceProvider.GetRequiredService();
+ private readonly IValidator _validator = serviceProvider.GetRequiredService>();
+ protected const string HandleMethodName = nameof(HandleAsync);
+
+ public async Task HandleAsync(
+ TRequest request,
+ CancellationToken cancellationToken
+ )
+ {
+ using var activity = ActivitySource.StartActivity($"{ClassName}");
+
+ Logs.StartingOperation(Logger, request.CorrelationId);
+ TResponseData response;
+
+ if (_validator != null)
+ {
+ var validationResult = await _validator.ValidateAsync(request, cancellationToken);
+ if (!validationResult.IsValid)
+ {
+ string errors = string.Join(", ", validationResult.Errors);
+
+ Logs.ValidationErrors(Logger, request.CorrelationId, errors);
+
+ response = Activator.CreateInstance();
+ response = response with
+ {
+ Success = false,
+ Message = errors
+ };
+
+ UseCaseFailedMetric.Add(1);
+
+ return response!;
+ }
+ }
+
+ response = await HandleInternalAsync(request, cancellationToken);
+
+ Logs.FinishedOperation(Logger, request.CorrelationId);
+
+ UseCaseExecutedMetric.Add(1);
+
+ return response;
+ }
+
+ public abstract Task HandleInternalAsync(TRequest request, CancellationToken cancellationToken);
+}
diff --git a/templates/Full/src/Application/Common/UseCases/BaseInUseCase.cs b/templates/Full/src/Application/Common/UseCases/BaseInUseCase.cs
new file mode 100644
index 00000000..c9efcbe7
--- /dev/null
+++ b/templates/Full/src/Application/Common/UseCases/BaseInUseCase.cs
@@ -0,0 +1,51 @@
+using Application.Common.Requests;
+using FluentValidation;
+using Microsoft.Extensions.DependencyInjection;
+using Application.Common.Services;
+using Application.Common.Repositories;
+using Application.Common.Helpers;
+
+namespace Application.Common.UseCases;
+
+public interface IBaseInUseCase where TRequest : BaseRequest
+{
+ Task HandleAsync(TRequest request, CancellationToken cancellationToken);
+}
+
+public abstract class BaseInUseCase(IServiceProvider serviceProvider) : BaseUseCase(serviceProvider), IBaseInUseCase where TRequest : BaseRequest
+{
+ protected IHybridCacheService Cache { get; } = serviceProvider.GetRequiredService();
+ protected IBaseRepository Repository { get; } = serviceProvider.GetRequiredService();
+ private readonly IValidator _validator = serviceProvider.GetRequiredService>();
+ protected const string HandleMethodName = nameof(HandleAsync);
+
+ public async Task HandleAsync(
+ TRequest request,
+ CancellationToken cancellationToken
+ )
+ {
+ using var activity = ActivitySource.StartActivity($"{ClassName}");
+
+ Logs.StartingOperation(Logger, request.CorrelationId);
+
+ if (_validator != null)
+ {
+ var validationResult = await _validator.ValidateAsync(request, cancellationToken);
+ if (!validationResult.IsValid)
+ {
+ var errors = string.Join(", ", validationResult.Errors);
+ Logs.ValidationErrors(Logger, request.CorrelationId, errors);
+ UseCaseFailedMetric.Add(1);
+ return;
+ }
+ }
+
+ await HandleInternalAsync(request, cancellationToken);
+
+ Logs.FinishedOperation(Logger, request.CorrelationId);
+
+ UseCaseExecutedMetric.Add(1);
+ }
+
+ public abstract Task HandleInternalAsync(TRequest request, CancellationToken cancellationToken);
+}
diff --git a/templates/Full/src/Application/Common/UseCases/BaseOutUseCase.cs b/templates/Full/src/Application/Common/UseCases/BaseOutUseCase.cs
new file mode 100644
index 00000000..636094c7
--- /dev/null
+++ b/templates/Full/src/Application/Common/UseCases/BaseOutUseCase.cs
@@ -0,0 +1,35 @@
+using Application.Common.Requests;
+using Application.Common.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Application.Common.Helpers;
+
+namespace Application.Common.UseCases;
+
+public interface IBaseOutUseCase where TResponseData : BaseResponse
+{
+ Task HandleAsync(CancellationToken cancellationToken);
+}
+
+public abstract class BaseOutUseCase(IServiceProvider serviceProvider) : BaseUseCase(serviceProvider), IBaseOutUseCase where TResponseData : BaseResponse
+{
+ protected IHybridCacheService Cache { get; } = serviceProvider.GetRequiredService();
+ protected const string HandleMethodName = nameof(HandleAsync);
+
+ public async Task HandleAsync(CancellationToken cancellationToken)
+ {
+ using var activity = ActivitySource.StartActivity($"{ClassName}");
+
+ var correlationId = Guid.NewGuid();
+ Logs.StartingOperation(Logger, correlationId);
+
+ var response = await HandleInternalAsync(cancellationToken);
+
+ Logs.FinishedOperation(Logger, correlationId);
+
+ UseCaseExecutedMetric.Add(1);
+
+ return response;
+ }
+
+ public abstract Task HandleInternalAsync(CancellationToken cancellationToken);
+}
diff --git a/templates/Full/src/Application/Common/UseCases/BaseUseCase.cs b/templates/Full/src/Application/Common/UseCases/BaseUseCase.cs
new file mode 100644
index 00000000..626ed4a5
--- /dev/null
+++ b/templates/Full/src/Application/Common/UseCases/BaseUseCase.cs
@@ -0,0 +1,57 @@
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+using Application.Common.Messages;
+using Application.Common.Services;
+using Domain.Common;
+using Domain.Common.Enums;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Application.Common.UseCases;
+
+public abstract class BaseUseCase
+{
+ protected IServiceProvider ServiceProvider { get; }
+ protected ILogger Logger { get; }
+ protected string ClassName { get; }
+ protected ActivitySource ActivitySource { get; } = DefaultConfigurations.ActivitySource;
+ protected IProduceService ProduceService { get; }
+ protected Counter UseCaseExecutedMetric { get; }
+ protected Counter UseCaseFailedMetric { get; }
+
+ protected BaseUseCase(IServiceProvider serviceProvider)
+ {
+ var classType = GetType();
+ ClassName = classType.Name;
+
+ ServiceProvider = serviceProvider;
+
+ Logger = serviceProvider.GetRequiredService().CreateLogger(classType);
+
+ ProduceService = serviceProvider.GetRequiredService();
+
+ UseCaseExecutedMetric = DefaultConfigurations.Meter
+ .CreateCounter($"{ClassName}.Executed", "total", "Number of times the use case was executed");
+
+ UseCaseFailedMetric = DefaultConfigurations.Meter
+ .CreateCounter($"{ClassName}.Failed", "total", "Number of times the use case execution failed");
+ }
+
+ protected void CreateNotification(
+ Guid correlationId,
+ string notificationStatus,
+ string createdBy,
+ NotificationType notificationType,
+ object message
+ ) => _ = ProduceService.HandleAsync(
+ new CreateNotificationMessage(
+ correlationId,
+ notificationType,
+ notificationStatus,
+ createdBy,
+ message
+ ),
+ CancellationToken.None,
+ queue: notificationType.ToString()
+ );
+}
\ No newline at end of file
diff --git a/templates/src/Application/Notifications/CreateNotificationUseCase.cs b/templates/Full/src/Application/Notifications/CreateNotificationUseCase.cs
similarity index 65%
rename from templates/src/Application/Notifications/CreateNotificationUseCase.cs
rename to templates/Full/src/Application/Notifications/CreateNotificationUseCase.cs
index 21d6d8cb..a0c389eb 100644
--- a/templates/src/Application/Notifications/CreateNotificationUseCase.cs
+++ b/templates/Full/src/Application/Notifications/CreateNotificationUseCase.cs
@@ -1,15 +1,15 @@
-using Application.Common.Constants;
using Application.Common.Requests;
using Application.Common.UseCases;
+using Application.Common.Helpers;
using Domain.Notifications;
using FluentValidation;
-using Microsoft.Extensions.Logging;
+using Domain.Common.Enums;
namespace Application.Notifications;
public sealed record CreateNotificationRequest(
Guid CorrelationId,
- string NotificationType,
+ NotificationType NotificationType,
string NotificationStatus,
string? CreatedBy = null,
object? Message = null
@@ -20,7 +20,7 @@ public sealed class CreateNotificationRequestValidator : AbstractValidator r.CorrelationId).NotEmpty();
- RuleFor(r => r.NotificationType).NotEmpty();
+ RuleFor(r => r.NotificationType).IsInEnum();
}
}
@@ -34,20 +34,14 @@ CancellationToken cancellationToken
var notification = new Notification(
request.NotificationType,
request.NotificationStatus,
+ request.Message,
request.CreatedBy,
- request.Message
+ request.TimezoneId
);
- var addResult = await _repository.AddAsync(notification, request.CorrelationId, cancellationToken);
+ var addResult = await Repository.AddAsync(notification, request.CorrelationId, cancellationToken);
if (addResult == 0)
- {
- logger.LogWarning(
- "[{ClassName}] | [{MethodName}] | [{CorrelationId}] | Failed to create notification.",
- ClassName,
- HandleMethodName,
- request.CorrelationId
- );
- }
+ Logs.FailedOperation(Logger, request.CorrelationId, "Failed to create notification. No rows affected.");
}
}
diff --git a/templates/src/Application/Notifications/GetAllNotificationsUseCase.cs b/templates/Full/src/Application/Notifications/GetAllNotificationsUseCase.cs
similarity index 76%
rename from templates/src/Application/Notifications/GetAllNotificationsUseCase.cs
rename to templates/Full/src/Application/Notifications/GetAllNotificationsUseCase.cs
index e6e45013..678aa011 100644
--- a/templates/src/Application/Notifications/GetAllNotificationsUseCase.cs
+++ b/templates/Full/src/Application/Notifications/GetAllNotificationsUseCase.cs
@@ -1,8 +1,7 @@
-using Application.Common.Constants;
using Application.Common.Requests;
using Application.Common.UseCases;
+using Application.Common.Helpers;
using Domain.Notifications;
-using Microsoft.Extensions.Logging;
namespace Application.Notifications;
@@ -14,7 +13,7 @@ public override async Task> HandleInterna
CancellationToken cancellationToken
)
{
- var (notifications, totalRecords) = await _repository.GetAllPaginatedAsync(
+ var (notifications, totalRecords) = await Repository.GetAllPaginatedAsync(
request.CorrelationId,
request.Page,
request.PageSize,
@@ -33,12 +32,7 @@ CancellationToken cancellationToken
if (notifications is null || !notifications.Any())
{
- logger.LogWarning(
- "[{ClassName}] | [{MethodName}] | [{CorrelationId}] | No notifications found.",
- ClassName,
- HandleMethodName,
- request.CorrelationId
- );
+ Logs.NotFound(Logger, request.CorrelationId, nameof(Notification));
return new(false, 0, 0, [], "No notifications found.");
}
diff --git a/templates/src/Application/Notifications/GetNotificationUseCase.cs b/templates/Full/src/Application/Notifications/GetNotificationUseCase.cs
similarity index 61%
rename from templates/src/Application/Notifications/GetNotificationUseCase.cs
rename to templates/Full/src/Application/Notifications/GetNotificationUseCase.cs
index 35b8f353..e047fe94 100644
--- a/templates/src/Application/Notifications/GetNotificationUseCase.cs
+++ b/templates/Full/src/Application/Notifications/GetNotificationUseCase.cs
@@ -1,12 +1,9 @@
-using System.Linq.Expressions;
-using Application.Common.Constants;
-using Application.Common.Repositories;
using Application.Common.Requests;
using Application.Common.UseCases;
+using Application.Common.Helpers;
using Domain.Notifications;
using FluentValidation;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
+using Microsoft.EntityFrameworkCore;
namespace Application.Notifications;
@@ -29,27 +26,20 @@ public override async Task> HandleInternalAsync(
CancellationToken cancellationToken
)
{
- var notification = await _repository.GetByIdAsNoTrackingAsync(
- request.Id,
- request.CorrelationId,
- n => new()
+ var notification = await Repository.GetQueryable(request.CorrelationId)
+ .Where(n => n.Id == request.Id)
+ .Select(n => new NotificationDto()
{
Id = n.Id,
Message = n.Message,
NotificationType = n.NotificationType,
- NotificationStatus = n.NotificationStatus,
- },
- cancellationToken
- );
+ NotificationStatus = n.NotificationStatus
+ })
+ .FirstOrDefaultAsync(cancellationToken);
if (notification is null)
{
- logger.LogWarning(
- "[{ClassName}] | [{MethodName}] | [{CorrelationId}] | Notification not found.",
- ClassName,
- HandleMethodName,
- request.CorrelationId
- );
+ Logs.NotFound(Logger, request.CorrelationId, nameof(notification));
return new(false, null, "Notification not found.");
}
diff --git a/templates/src/Application/Notifications/NotificationDto.cs b/templates/Full/src/Application/Notifications/NotificationDto.cs
similarity index 70%
rename from templates/src/Application/Notifications/NotificationDto.cs
rename to templates/Full/src/Application/Notifications/NotificationDto.cs
index 46dc9967..f87f8696 100644
--- a/templates/src/Application/Notifications/NotificationDto.cs
+++ b/templates/Full/src/Application/Notifications/NotificationDto.cs
@@ -1,9 +1,11 @@
+using Domain.Common.Enums;
+
namespace Application.Notifications;
public sealed record NotificationDto
{
public int Id { get; set; }
- public string NotificationType { get; set; }
+ public NotificationType NotificationType { get; set; }
public string NotificationStatus { get; set; }
public string Message { get; set; }
};
diff --git a/templates/src/Application/Orders/CreateOrderUseCase.cs b/templates/Full/src/Application/Orders/CreateOrderUseCase.cs
similarity index 57%
rename from templates/src/Application/Orders/CreateOrderUseCase.cs
rename to templates/Full/src/Application/Orders/CreateOrderUseCase.cs
index e2dfb7e7..f13f3dbd 100644
--- a/templates/src/Application/Orders/CreateOrderUseCase.cs
+++ b/templates/Full/src/Application/Orders/CreateOrderUseCase.cs
@@ -1,14 +1,19 @@
-using Application.Common.Constants;
-using Application.Common.Messages;
+using Application.Common.Helpers;
using Application.Common.Requests;
using Application.Common.UseCases;
+using Domain.Common.Enums;
using Domain.Orders;
using FluentValidation;
-using Microsoft.Extensions.Logging;
namespace Application.Orders;
-public sealed record CreateOrderRequest(Guid CorrelationId, string Description, CreateOrderItemRequest[] Items) : BaseRequest(CorrelationId);
+public sealed record CreateOrderRequest(
+ Guid CorrelationId,
+ string Description,
+ CreateOrderItemRequest[] Items,
+ string CreatedBy = "",
+ string TimezoneId = ""
+) : BaseRequest(CorrelationId, CreatedBy, TimezoneId);
public sealed record CreateOrderItemRequest(string Name, string Description, decimal Value);
@@ -35,39 +40,46 @@ public CreateOrderRequestValidator()
public sealed class CreateOrderUseCase(IServiceProvider serviceProvider)
: BaseInOutUseCase>(serviceProvider)
{
+ private readonly NotificationType _notificationType = NotificationType.OrderCreated;
public override async Task> HandleInternalAsync(
CreateOrderRequest request,
CancellationToken cancellationToken
)
{
- Guid correlationId = request.CorrelationId;
+ var correlationId = request.CorrelationId;
BaseResponse response;
var items = request.Items
.Select(i => new Item(i.Name, i.Description, i.Value))
.ToList();
- var newOrder = new Order(request.Description, items);
- var createResult = newOrder.SetTotal();
+ var createResult = Order.Create(
+ request.Description, items,
+ request.CreatedBy, request.TimezoneId
+ );
if (createResult.IsFailure)
{
- logger.LogWarning("[{ClassName}] | [{MethodName}] | [{CorrelationId}] | {Message}", ClassName, HandleMethodName, correlationId, createResult.Message);
+ Logs.FailedOperation(Logger, correlationId, createResult.Message);
response = new(false, null, createResult.Message);
- CreateNotification(correlationId, "Failed", response);
+ CreateNotification(correlationId, "Failed", request.CreatedBy, _notificationType, response);
+
+ UseCaseFailedMetric.Add(1);
return response;
}
- var addResult = await _repository.AddAsync(newOrder, correlationId, cancellationToken);
- if (addResult == 0)
+ var newOrder = createResult.Value;
+ if (await Repository.AddAsync(newOrder, correlationId, cancellationToken) == 0)
{
- logger.LogWarning("[{ClassName}] | [{MethodName}] | [{CorrelationId}] | " + "Failed to create order.", ClassName, HandleMethodName, correlationId);
+ Logs.FailedOperation(Logger, correlationId, "Failed to create order. No rows affected.");
response = new(false, null, "Failed to create order.");
- CreateNotification(correlationId, "Failed", response);
+ CreateNotification(correlationId, "Failed", request.CreatedBy, _notificationType, response);
+
+ UseCaseFailedMetric.Add(1);
return response;
}
@@ -85,20 +97,8 @@ CancellationToken cancellationToken
})]
});
- CreateNotification(correlationId, "Success", response);
+ CreateNotification(correlationId, "Success", request.CreatedBy, _notificationType, response);
return response;
}
-
- private void CreateNotification(Guid correlationId, string notificationStatus, object message) => _ = _produceService.HandleAsync(
- new CreateNotificationMessage(
- correlationId,
- NotificationType.OrderCreated,
- notificationStatus,
- "System",
- message
- ),
- CancellationToken.None,
- NotificationType.OrderCreated
- );
}
diff --git a/templates/src/Application/Orders/GetAllOrdersUseCase.cs b/templates/Full/src/Application/Orders/GetAllOrdersUseCase.cs
similarity index 67%
rename from templates/src/Application/Orders/GetAllOrdersUseCase.cs
rename to templates/Full/src/Application/Orders/GetAllOrdersUseCase.cs
index ee5f2e78..e55965b1 100644
--- a/templates/src/Application/Orders/GetAllOrdersUseCase.cs
+++ b/templates/Full/src/Application/Orders/GetAllOrdersUseCase.cs
@@ -1,8 +1,7 @@
-using Application.Common.Constants;
using Application.Common.Requests;
using Application.Common.UseCases;
+using Application.Common.Helpers;
using Domain.Orders;
-using Microsoft.Extensions.Logging;
namespace Application.Orders;
@@ -14,7 +13,7 @@ public override async Task> HandleInternalAsync(
CancellationToken cancellationToken
)
{
- var (orders, totalRecords) = await _repository.GetAllPaginatedAsync(
+ var (orders, totalRecords) = await Repository.GetAllPaginatedAsync(
request.CorrelationId,
request.Page,
request.PageSize,
@@ -31,16 +30,11 @@ CancellationToken cancellationToken
if (orders is null || !orders.Any())
{
- logger.LogWarning(
- "[{ClassName}] | [{MethodName}] | [{CorrelationId}] | No orders found.",
- ClassName,
- HandleMethodName,
- request.CorrelationId
- );
+ Logs.NotFound(Logger, request.CorrelationId, nameof(orders));
return new(false, 0, 0, [], "No orders found.");
}
- var totalPages = (int)Math.Ceiling(totalRecords / (double)request.PageSize);
+ var totalPages = (int) Math.Ceiling(totalRecords / (double) request.PageSize);
return new(true, totalPages, totalRecords, orders);
}
diff --git a/templates/src/Application/Orders/GetOrderUseCase.cs b/templates/Full/src/Application/Orders/GetOrderUseCase.cs
similarity index 55%
rename from templates/src/Application/Orders/GetOrderUseCase.cs
rename to templates/Full/src/Application/Orders/GetOrderUseCase.cs
index 7dd8c980..8ecc4919 100644
--- a/templates/src/Application/Orders/GetOrderUseCase.cs
+++ b/templates/Full/src/Application/Orders/GetOrderUseCase.cs
@@ -1,8 +1,9 @@
using Application.Common.Requests;
using Application.Common.UseCases;
+using Application.Common.Helpers;
using Domain.Orders;
using FluentValidation;
-using Microsoft.Extensions.Logging;
+using Microsoft.EntityFrameworkCore;
namespace Application.Orders;
@@ -23,29 +24,24 @@ public override async Task> HandleInternalAsync(
CancellationToken cancellationToken
)
{
- var order = await _repository.GetByIdAsNoTrackingAsync(
- request.Id,
- request.CorrelationId,
- o => new OrderDto()
+ var order = await Repository.GetQueryable(request.CorrelationId)
+ .Select(o => new OrderDto
+ {
+ Id = o.Id,
+ Description = o.Description,
+ Total = o.Total,
+ Items = o.Items.Select(i => new ItemDto
{
- Id = o.Id,
- Total = o.Total,
- Items = o.Items.Select(i => new ItemDto
- {
- Id = i.Id,
- Name = i.Name,
- Value = i.Value
- }).ToArray()
- },
- cancellationToken
- );
+ Id = i.Id,
+ Name = i.Name,
+ Description = i.Description,
+ Value = i.Value
+ }).ToList()
+ }).FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);
if (order is null)
{
- logger.LogWarning(
- "[{ClassName}] | [{MethodName}] | [{CorrelationId}] | Order not found.",
- ClassName, HandleMethodName, request.CorrelationId
- );
+ Logs.NotFound(Logger, request.CorrelationId, nameof(order));
return new(false, null, "Order not found.");
}
diff --git a/templates/src/Application/Orders/OrderDto.cs b/templates/Full/src/Application/Orders/OrderDto.cs
similarity index 100%
rename from templates/src/Application/Orders/OrderDto.cs
rename to templates/Full/src/Application/Orders/OrderDto.cs
diff --git a/templates/Full/src/Application/packages.lock.json b/templates/Full/src/Application/packages.lock.json
new file mode 100644
index 00000000..57833687
--- /dev/null
+++ b/templates/Full/src/Application/packages.lock.json
@@ -0,0 +1,118 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "FluentValidation": {
+ "type": "Direct",
+ "requested": "[12.1.1, )",
+ "resolved": "12.1.1",
+ "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw=="
+ },
+ "FluentValidation.DependencyInjectionExtensions": {
+ "type": "Direct",
+ "requested": "[12.1.1, )",
+ "resolved": "12.1.1",
+ "contentHash": "D0VXh4dtjjX2aQizuaa0g6R8X3U1JaVqJPfGCvLwZX9t/O2h7tkpbitbadQMfwcgSPdDbI2vDxuwRMv/Uf9dHA==",
+ "dependencies": {
+ "FluentValidation": "12.1.1",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.0"
+ }
+ },
+ "Microsoft.EntityFrameworkCore": {
+ "type": "Direct",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5",
+ "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Logging": {
+ "type": "Direct",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag=="
+ },
+ "Microsoft.EntityFrameworkCore.Analyzers": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg=="
+ },
+ "Microsoft.Extensions.Caching.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Caching.Memory": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA=="
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Options": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g=="
+ },
+ "domain": {
+ "type": "Project",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection": "[10.0.5, )"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/templates/Full/src/Domain/Common/DefaultConfigurations.cs b/templates/Full/src/Domain/Common/DefaultConfigurations.cs
new file mode 100644
index 00000000..a20fa200
--- /dev/null
+++ b/templates/Full/src/Domain/Common/DefaultConfigurations.cs
@@ -0,0 +1,12 @@
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+
+namespace Domain.Common;
+
+public static class DefaultConfigurations
+{
+ public static string ApplicationName => "Hexagonal.Solution.Template";
+ public static string Version => typeof(DefaultConfigurations).Assembly.GetName().Version!.ToString();
+ public static readonly Meter Meter = new(ApplicationName, Version);
+ public static readonly ActivitySource ActivitySource = new(ApplicationName, Version);
+}
diff --git a/templates/Full/src/Domain/Common/DomainEntity.cs b/templates/Full/src/Domain/Common/DomainEntity.cs
new file mode 100644
index 00000000..183a9b33
--- /dev/null
+++ b/templates/Full/src/Domain/Common/DomainEntity.cs
@@ -0,0 +1,63 @@
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace Domain.Common;
+
+public abstract class DomainEntity
+{
+ protected virtual string EntityName { get; }
+ protected DomainEntity()
+ {
+ EntityName = GetType().Name;
+ }
+ protected DomainEntity(string user, string? timezoneId = null)
+ {
+ EntityName = GetType().Name;
+ CreatedAt = DateTime.UtcNow;
+ CreatedBy = user;
+ CreatedByTimezoneId = TimeZoneInfo.FindSystemTimeZoneById(string.IsNullOrWhiteSpace(timezoneId) ? TimeZoneInfo.Utc.Id : timezoneId).Id;
+ UpdatedAt = CreatedAt;
+ UpdatedBy = CreatedBy;
+ UpdatedByTimezoneId = CreatedByTimezoneId;
+ }
+ protected static ActivitySource ActivitySource { get; } = DefaultConfigurations.ActivitySource;
+ public int Id { get; init; }
+ public DateTime CreatedAt { get; init; }
+ public string? CreatedBy { get; init; }
+ public string CreatedByTimezoneId { get; init; }
+ public DateTime UpdatedAt { get; private set; }
+ public string? UpdatedBy { get; private set; }
+ public string? UpdatedByTimezoneId { get; private set; }
+
+ protected static Result Handle(
+ Func> factory,
+ [CallerMemberName] string callerName = null!
+ ) where TEntity : DomainEntity
+ {
+ using var activity = ActivitySource.StartActivity($"{typeof(TEntity).Name}.{callerName}");
+
+ return factory(activity);
+ }
+
+ protected static Result Handle(
+ Func factory,
+ [CallerMemberName] string callerName = null!
+ )
+ {
+ using var activity = ActivitySource.StartActivity($"{typeof(DomainEntity).Name}.{callerName}");
+
+ return factory(activity);
+ }
+
+ public Result Update(string? user = null, string? timezoneId = null) => Handle(activity =>
+ {
+ UpdatedAt = DateTime.UtcNow;
+ UpdatedBy = user ?? "System";
+ UpdatedByTimezoneId = TimeZoneInfo.FindSystemTimeZoneById(string.IsNullOrWhiteSpace(timezoneId) ? TimeZoneInfo.Utc.Id : timezoneId).Id;
+
+ activity?.SetTag(nameof(UpdatedBy), UpdatedBy);
+ activity?.SetTag(nameof(UpdatedByTimezoneId), UpdatedByTimezoneId);
+
+ return Result.Ok();
+ });
+}
diff --git a/templates/Full/src/Domain/Common/DomainException.cs b/templates/Full/src/Domain/Common/DomainException.cs
new file mode 100644
index 00000000..d4094e14
--- /dev/null
+++ b/templates/Full/src/Domain/Common/DomainException.cs
@@ -0,0 +1,3 @@
+namespace Domain.Common;
+
+public sealed class DomainException(string message) : Exception(message);
\ No newline at end of file
diff --git a/templates/Full/src/Domain/Common/Enums/NotificationType.cs b/templates/Full/src/Domain/Common/Enums/NotificationType.cs
new file mode 100644
index 00000000..0b7dc50d
--- /dev/null
+++ b/templates/Full/src/Domain/Common/Enums/NotificationType.cs
@@ -0,0 +1,6 @@
+namespace Domain.Common.Enums;
+
+public enum NotificationType
+{
+ OrderCreated
+}
diff --git a/templates/src/Domain/Common/Result.cs b/templates/Full/src/Domain/Common/Result.cs
similarity index 100%
rename from templates/src/Domain/Common/Result.cs
rename to templates/Full/src/Domain/Common/Result.cs
diff --git a/templates/src/Domain/Domain.csproj b/templates/Full/src/Domain/Domain.csproj
similarity index 100%
rename from templates/src/Domain/Domain.csproj
rename to templates/Full/src/Domain/Domain.csproj
diff --git a/templates/src/Domain/DomainDependencyInjection.cs b/templates/Full/src/Domain/DomainDependencyInjection.cs
similarity index 100%
rename from templates/src/Domain/DomainDependencyInjection.cs
rename to templates/Full/src/Domain/DomainDependencyInjection.cs
diff --git a/templates/src/Domain/Notifications/Notification.cs b/templates/Full/src/Domain/Notifications/Notification.cs
similarity index 52%
rename from templates/src/Domain/Notifications/Notification.cs
rename to templates/Full/src/Domain/Notifications/Notification.cs
index a7a3f766..e62e56be 100644
--- a/templates/src/Domain/Notifications/Notification.cs
+++ b/templates/Full/src/Domain/Notifications/Notification.cs
@@ -1,5 +1,6 @@
using System.Text.Json;
using Domain.Common;
+using Domain.Common.Enums;
namespace Domain.Notifications;
@@ -8,17 +9,23 @@ public sealed class Notification : DomainEntity
public Notification() {}
public Notification(
- string notificationType,
+ NotificationType notificationType,
string notificationStatus,
+ object? message = null,
string? createdBy = null,
- object? message = null
- ) : base(DateTime.UtcNow, createdBy)
+ string? timezoneId = null
+ ) : base(createdBy ?? "System", timezoneId)
{
+ using var activity = ActivitySource.StartActivity($"{GetType().Name}.Constructor");
+
NotificationType = notificationType;
NotificationStatus = notificationStatus;
Message = message != null ? JsonSerializer.Serialize(message) : string.Empty;
+
+ activity?.SetTag(nameof(NotificationType), NotificationType);
+ activity?.SetTag(nameof(NotificationStatus), NotificationStatus);
}
- public string NotificationType { get; init; }
+ public NotificationType NotificationType { get; init; }
public string NotificationStatus { get; init; }
public string Message { get; init; }
}
diff --git a/templates/src/Domain/Orders/Item.cs b/templates/Full/src/Domain/Orders/Item.cs
similarity index 53%
rename from templates/src/Domain/Orders/Item.cs
rename to templates/Full/src/Domain/Orders/Item.cs
index 9b58c181..c8e6c76e 100644
--- a/templates/src/Domain/Orders/Item.cs
+++ b/templates/Full/src/Domain/Orders/Item.cs
@@ -5,10 +5,20 @@ public sealed class Item : DomainEntity
{
public Item() { }
- public Item(string name, string description, decimal value) : base(DateTime.UtcNow)
+ public Item(
+ string name,
+ string description,
+ decimal value,
+ string? createdBy = null,
+ string? timezoneId = null
+ ) : base(createdBy ?? "System", timezoneId)
{
Name = name;
Description = description;
+
+ if (value <= 0)
+ throw new DomainException("Item value cannot be zero or negative.");
+
Value = value;
}
diff --git a/templates/Full/src/Domain/Orders/Order.cs b/templates/Full/src/Domain/Orders/Order.cs
new file mode 100644
index 00000000..10ef0d51
--- /dev/null
+++ b/templates/Full/src/Domain/Orders/Order.cs
@@ -0,0 +1,57 @@
+using Domain.Common;
+
+namespace Domain.Orders;
+
+public sealed class Order : DomainEntity
+{
+ public Order() { }
+
+ private Order(
+ string description,
+ ICollection- items,
+ string? createdBy = null,
+ string? timezoneId = null
+ ) : base(createdBy ?? "System", timezoneId)
+ {
+ Description = description;
+ Items = items;
+ }
+
+ public string Description { get; private set; }
+ public decimal Total { get; private set; }
+ public ICollection
- Items { get; private set; }
+
+ public static Result Create(
+ string description,
+ ICollection
- items,
+ string user = "System",
+ string? timezoneId = null
+ ) => Handle(activity =>
+ {
+ Order order = new(description, items, user, timezoneId);
+
+ var setTotalResult = order.SetTotal(user, timezoneId);
+ if (setTotalResult.IsFailure)
+ return Result.Fail(setTotalResult.Message);
+
+ activity?.SetTag(nameof(description), description);
+
+ return Result.Ok(order);
+ });
+
+ public Result SetTotal(string user = "System", string? timezoneId = null) => Handle(activity =>
+ {
+ if (Items == null || Items.Count == 0)
+ return Result.Fail("Order must have at least one item.");
+
+ Total = Items.Sum(item => item.Value);
+
+ var result = Update(user, timezoneId);
+ if (result.IsFailure)
+ return Result.Fail(result.Message);
+
+ activity?.SetTag(nameof(Total), Total);
+
+ return Result.Ok();
+ });
+}
diff --git a/templates/Full/src/Domain/packages.lock.json b/templates/Full/src/Domain/packages.lock.json
new file mode 100644
index 00000000..34de2c6c
--- /dev/null
+++ b/templates/Full/src/Domain/packages.lock.json
@@ -0,0 +1,21 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "Microsoft.Extensions.DependencyInjection": {
+ "type": "Direct",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA=="
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/templates/src/Infrastructure/Cache/InfrastructureCacheDependencyInjection.cs b/templates/Full/src/Infrastructure/Cache/InfrastructureCacheDependencyInjection.cs
similarity index 91%
rename from templates/src/Infrastructure/Cache/InfrastructureCacheDependencyInjection.cs
rename to templates/Full/src/Infrastructure/Cache/InfrastructureCacheDependencyInjection.cs
index ce4802ad..b32f1a66 100644
--- a/templates/src/Infrastructure/Cache/InfrastructureCacheDependencyInjection.cs
+++ b/templates/Full/src/Infrastructure/Cache/InfrastructureCacheDependencyInjection.cs
@@ -14,7 +14,7 @@ public IServiceCollection AddCache(IConfiguration configuration)
services
.AddStackExchangeRedisCache(options =>
{
- options.Configuration = configuration.GetConnectionString("Redis") ?? throw new NullReferenceException("Redis connection string is not configured.");
+ options.Configuration = configuration.GetConnectionString("Redis") ?? throw new InvalidOperationException("Redis connection string is not configured.");
options.Configuration += ",abortConnect=false,connectTimeout=5000,syncTimeout=5000";
})
.AddHybridCache(options =>
diff --git a/templates/Full/src/Infrastructure/Cache/Services/HybridCacheService.cs b/templates/Full/src/Infrastructure/Cache/Services/HybridCacheService.cs
new file mode 100644
index 00000000..9bfa2ce9
--- /dev/null
+++ b/templates/Full/src/Infrastructure/Cache/Services/HybridCacheService.cs
@@ -0,0 +1,60 @@
+using System.Diagnostics;
+using Application.Common.Helpers;
+using Application.Common.Services;
+using Domain.Common;
+using Microsoft.Extensions.Caching.Hybrid;
+using Microsoft.Extensions.Logging;
+
+namespace Infrastructure.Cache.Services;
+
+internal sealed class HybridCacheService(HybridCache cache, ILogger logger) : IHybridCacheService
+{
+ private readonly HybridCache _cache = cache;
+ private readonly ILogger _logger = logger;
+ private readonly string _className = nameof(HybridCacheService);
+ private readonly ActivitySource _activities = DefaultConfigurations.ActivitySource;
+ public async ValueTask GetOrCreateAsync(
+ Guid correlationId,
+ string key,
+ Func> factory,
+ CancellationToken cancellationToken
+ )
+ {
+ using var activity = _activities.StartActivity($"{_className}.{nameof(GetOrCreateAsync)}");
+
+ Logs.DebugStartingOperation(_logger, correlationId, key);
+ var result = await _cache.GetOrCreateAsync($"{DefaultConfigurations.ApplicationName}:{key}", factory, cancellationToken: cancellationToken);
+
+ Logs.DebugFinishedOperation(_logger, correlationId, $"Cache hit: {result != null} for key: {key}");
+
+ activity?.SetTag("key", key);
+
+ return result;
+ }
+
+ public async ValueTask CreateAsync(Guid correlationId, string key, TResult value, CancellationToken cancellationToken)
+ {
+ using var activity = _activities.StartActivity($"{_className}.{nameof(CreateAsync)}");
+
+ Logs.DebugStartingOperation(_logger, correlationId, key);
+
+ await _cache.SetAsync($"{DefaultConfigurations.ApplicationName}:{key}", value, cancellationToken: cancellationToken);
+
+ Logs.DebugFinishedOperation(_logger, correlationId, $"Cached hit: {value != null} for key: {key}");
+
+ activity?.SetTag("key", key);
+ }
+
+ public async ValueTask DeleteAsync(Guid correlationId, string key, CancellationToken cancellationToken)
+ {
+ using var activity = _activities.StartActivity($"{_className}.{nameof(DeleteAsync)}");
+
+ Logs.DebugStartingOperation(_logger, correlationId, key);
+
+ await _cache.RemoveAsync($"{DefaultConfigurations.ApplicationName}:{key}", cancellationToken);
+
+ Logs.DebugFinishedOperation(_logger, correlationId, $"Cache entry removed for key: {key}");
+
+ activity?.SetTag("key", key);
+ }
+}
diff --git a/templates/src/Infrastructure/Common/BaseBackgroundService.cs b/templates/Full/src/Infrastructure/Common/BaseBackgroundService.cs
similarity index 81%
rename from templates/src/Infrastructure/Common/BaseBackgroundService.cs
rename to templates/Full/src/Infrastructure/Common/BaseBackgroundService.cs
index fa375965..45aae6e7 100644
--- a/templates/src/Infrastructure/Common/BaseBackgroundService.cs
+++ b/templates/Full/src/Infrastructure/Common/BaseBackgroundService.cs
@@ -1,4 +1,4 @@
-using System.Diagnostics;
+using Application.Common.Helpers;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -28,10 +28,7 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
}
catch (Exception ex)
{
- logger.LogError(
- "[BaseBackgroundService] | [ExecuteAsync] | Unexpected error in background service. | Message: {ErrorMessage} | StackTrace: {StackTrace}",
- ex.Message, ex.StackTrace
- );
+ Logs.Error(logger, ex.Message);
throw;
}
diff --git a/templates/src/Infrastructure/Data/Common/BaseMapping.cs b/templates/Full/src/Infrastructure/Data/Common/BaseMapping.cs
similarity index 100%
rename from templates/src/Infrastructure/Data/Common/BaseMapping.cs
rename to templates/Full/src/Infrastructure/Data/Common/BaseMapping.cs
diff --git a/templates/Full/src/Infrastructure/Data/Common/BaseRepository.cs b/templates/Full/src/Infrastructure/Data/Common/BaseRepository.cs
new file mode 100644
index 00000000..32802ae5
--- /dev/null
+++ b/templates/Full/src/Infrastructure/Data/Common/BaseRepository.cs
@@ -0,0 +1,190 @@
+using System.Diagnostics;
+using System.Linq.Expressions;
+using System.Runtime.CompilerServices;
+using Application.Common.Helpers;
+using Application.Common.Repositories;
+using Domain.Common;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Infrastructure.Data.Common;
+
+public class BaseRepository(
+ ILogger logger,
+ IDbContextFactory dbContextFactory
+) : IBaseRepository
+{
+ private readonly IDbContextFactory _dbContextFactory = dbContextFactory;
+ private readonly MyDbContext _dbContext = dbContextFactory.CreateDbContext();
+ private readonly string _className = nameof(BaseRepository);
+ private readonly ActivitySource _activities = DefaultConfigurations.ActivitySource;
+
+ private async Task HandleBaseQueryAsync(
+ Func, Task> query,
+ Guid correlationId,
+ bool? newContext = false,
+ [CallerMemberName]
+ string methodName = null!
+ ) where TEntity : DomainEntity
+ {
+ using var activity = _activities.StartActivity($"{_className}.{methodName}.{typeof(TEntity).Name}");
+
+ Logs.DebugStartingOperation(logger, correlationId);
+
+ var dbSet = _dbContext.Set();
+ if (newContext.GetValueOrDefault())
+ dbSet = _dbContextFactory.CreateDbContext().Set();
+
+ var result = await query.Invoke(dbSet);
+
+ Logs.DebugFinishedOperation(logger, correlationId);
+
+ activity?.SetTag("correlationId", correlationId);
+ activity?.Stop();
+
+ return result;
+ }
+
+ public IQueryable GetQueryable(
+ Guid correlationId,
+ bool? newContext = null,
+ [CallerMemberName]
+ string methodName = null!
+ ) where TEntity : DomainEntity
+ {
+ using var activity = _activities.StartActivity($"{_className}.{nameof(GetQueryable)}");
+
+ Logs.DebugStartingOperation(logger, correlationId);
+
+ var dbSet = _dbContext.Set();
+ if (newContext.GetValueOrDefault())
+ dbSet = _dbContextFactory.CreateDbContext().Set();
+
+ Logs.DebugFinishedOperation(logger, correlationId);
+
+ return dbSet;
+ }
+
+ public async Task AddAsync(TEntity entity, Guid correlationId, CancellationToken cancellationToken, bool? newContext = null) where TEntity : DomainEntity =>
+ await HandleBaseQueryAsync(async dbEntitySet =>
+ {
+ await dbEntitySet.AddAsync(entity, cancellationToken);
+
+ return await _dbContext.SaveChangesAsync(cancellationToken);
+ }, correlationId, newContext);
+
+ public async Task AddRangeAsync(TEntity[] entities, Guid correlationId, CancellationToken cancellationToken, bool? newContext = null) where TEntity : DomainEntity =>
+ await HandleBaseQueryAsync(async dbEntitySet =>
+ {
+ await dbEntitySet.AddRangeAsync(entities, cancellationToken);
+
+ return await _dbContext.SaveChangesAsync(cancellationToken);
+ }, correlationId, newContext);
+
+ public async Task UpdateAsync(TEntity entity, Guid correlationId, CancellationToken cancellationToken, bool? newContext = null) where TEntity : DomainEntity =>
+ await HandleBaseQueryAsync(async dbEntitySet =>
+ {
+ var updatedEntity = dbEntitySet.Update(entity);
+
+ return await _dbContext.SaveChangesAsync(cancellationToken);
+ }, correlationId, newContext);
+
+ public async Task RemoveAsync(TEntity entity, Guid correlationId, CancellationToken cancellationToken, bool? newContext = null) where TEntity : DomainEntity =>
+ await HandleBaseQueryAsync(async dbEntitySet =>
+ {
+ dbEntitySet.Remove(entity);
+
+ return await _dbContext.SaveChangesAsync(cancellationToken);
+ }, correlationId, newContext);
+
+ public async Task RemoveRangeAsync(TEntity[] entities, Guid correlationId, CancellationToken cancellationToken, bool? newContext = null) where TEntity : DomainEntity =>
+ await HandleBaseQueryAsync(async dbEntitySet =>
+ {
+ dbEntitySet.RemoveRange(entities);
+
+ return await _dbContext.SaveChangesAsync(cancellationToken);
+ }, correlationId, newContext);
+
+ public async Task<(IEnumerable Items, int TotalRecords)> GetAllPaginatedAsync(
+ Guid correlationId,
+ int page,
+ int pageSize,
+ CancellationToken cancellationToken,
+ string? sortBy = null!,
+ bool sortDescending = false,
+ Dictionary? searchByValues = null!,
+ bool? newContext = null,
+ params Expression>[]? includes
+ ) where TEntity : DomainEntity => await HandleBaseQueryAsync Items, int TotalRecords)>(async dbEntitySet =>
+ {
+ var query = dbEntitySet.AsNoTracking();
+
+ if (includes is not null)
+ foreach (var include in includes)
+ query = query.Include(include);
+
+ if (!string.IsNullOrWhiteSpace(sortBy))
+ query = sortDescending
+ ? query.OrderByDescending(e => EF.Property