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(e, sortBy)) + : query.OrderBy(e => EF.Property(e, sortBy)); + else + query = query.OrderBy(e => e.CreatedAt); + + var totalRecords = await query.CountAsync(cancellationToken); + + if (searchByValues != null && searchByValues.Count != 0) + foreach (var searchByValue in searchByValues) + query = query.Where(e => + EF.Functions.ILike(EF.Property(e, searchByValue.Key), $"%{searchByValue.Value}%") + ); + + var items = await query + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); + + return (items, totalRecords); + }, correlationId, newContext); + + public async 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 => await HandleBaseQueryAsync Items, int TotalRecords)>(async dbEntitySet => + { + var query = dbEntitySet.AsQueryable(); + + if (predicate != null) + query = query.Where(predicate); + + if (!string.IsNullOrWhiteSpace(sortBy)) + query = sortDescending + ? query.OrderByDescending(e => EF.Property(e, sortBy)) + : query.OrderBy(e => EF.Property(e, sortBy)); + else + query = query.OrderBy(e => e.CreatedAt); + + var totalRecords = await query.CountAsync(cancellationToken); + + if (searchByValues != null && searchByValues.Count != 0) + foreach (var searchByValue in searchByValues) + query = query.Where(e => + EF.Functions.ILike(EF.Property(e, searchByValue.Key), $"%{searchByValue.Value}%") + ); + + var items = await query + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(selector) + .ToListAsync(cancellationToken); + + return (items, totalRecords); + }, correlationId, newContext); +} diff --git a/templates/src/Infrastructure/Data/InfrastructureDataDependencyInjection.cs b/templates/Full/src/Infrastructure/Data/InfrastructureDataDependencyInjection.cs similarity index 78% rename from templates/src/Infrastructure/Data/InfrastructureDataDependencyInjection.cs rename to templates/Full/src/Infrastructure/Data/InfrastructureDataDependencyInjection.cs index 775c688e..028724a5 100644 --- a/templates/src/Infrastructure/Data/InfrastructureDataDependencyInjection.cs +++ b/templates/Full/src/Infrastructure/Data/InfrastructureDataDependencyInjection.cs @@ -11,15 +11,15 @@ internal static class InfrastructureDataDependencyInjection { public IServiceCollection AddData(IConfiguration configuration) { - bool.TryParse( + var enableSensitiveDataLogging = bool.TryParse( Environment.GetEnvironmentVariable("ENABLE_SENSITIVE_DATA_LOGGING"), - out var enableSensitiveDataLogging - ); + out var parsedValue + ) && parsedValue; services.AddPooledDbContextFactory(options => { - options.UseSqlServer( - configuration.GetConnectionString("OrderDb") ?? throw new NullReferenceException("OrderDb connection string is not configured.") + options.UseNpgsql( + configuration.GetConnectionString("OrderDb") ?? throw new InvalidOperationException("OrderDb connection string is not configured.") ); options.EnableSensitiveDataLogging(enableSensitiveDataLogging); }); diff --git a/templates/src/Infrastructure/Data/Mapping/ItemDbMapping.cs b/templates/Full/src/Infrastructure/Data/Mapping/ItemDbMapping.cs similarity index 100% rename from templates/src/Infrastructure/Data/Mapping/ItemDbMapping.cs rename to templates/Full/src/Infrastructure/Data/Mapping/ItemDbMapping.cs diff --git a/templates/src/Infrastructure/Data/Mapping/NotificationDbMapping.cs b/templates/Full/src/Infrastructure/Data/Mapping/NotificationDbMapping.cs similarity index 95% rename from templates/src/Infrastructure/Data/Mapping/NotificationDbMapping.cs rename to templates/Full/src/Infrastructure/Data/Mapping/NotificationDbMapping.cs index 2d6dbc04..075dee99 100644 --- a/templates/src/Infrastructure/Data/Mapping/NotificationDbMapping.cs +++ b/templates/Full/src/Infrastructure/Data/Mapping/NotificationDbMapping.cs @@ -9,7 +9,6 @@ internal sealed class NotificationDbMapping : BaseDbMapping public override void ConfigureDomainEntity(EntityTypeBuilder builder) { builder.Property(p => p.NotificationType) - .HasMaxLength(100) .IsRequired(); builder.Property(p => p.NotificationStatus) diff --git a/templates/src/Infrastructure/Data/Mapping/OrderDbMapping.cs b/templates/Full/src/Infrastructure/Data/Mapping/OrderDbMapping.cs similarity index 100% rename from templates/src/Infrastructure/Data/Mapping/OrderDbMapping.cs rename to templates/Full/src/Infrastructure/Data/Mapping/OrderDbMapping.cs diff --git a/templates/src/Infrastructure/Data/Migrations/20251012134409_AddNotificationTable.Designer.cs b/templates/Full/src/Infrastructure/Data/Migrations/20260403110832_CreateTables.Designer.cs similarity index 56% rename from templates/src/Infrastructure/Data/Migrations/20251012134409_AddNotificationTable.Designer.cs rename to templates/Full/src/Infrastructure/Data/Migrations/20260403110832_CreateTables.Designer.cs index 45ba980b..0b964959 100644 --- a/templates/src/Infrastructure/Data/Migrations/20251012134409_AddNotificationTable.Designer.cs +++ b/templates/Full/src/Infrastructure/Data/Migrations/20260403110832_CreateTables.Designer.cs @@ -3,63 +3,70 @@ using Infrastructure.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable namespace Infrastructure.Data.Migrations { [DbContext(typeof(MyDbContext))] - [Migration("20251012134409_AddNotificationTable")] - partial class AddNotificationTable + [Migration("20260403110832_CreateTables")] + partial class CreateTables { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.8") - .HasAnnotation("Relational:MaxIdentifierLength", 128); + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("Domain.Notifications.Notification", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("int"); + .HasColumnType("integer"); - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("CreatedAt") - .HasColumnType("datetime2"); + .HasColumnType("timestamp with time zone"); b.Property("CreatedBy") .HasMaxLength(100) - .HasColumnType("varchar"); + .HasColumnType("text"); + + b.Property("CreatedByTimezoneId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); b.Property("Message") .HasMaxLength(4000) - .HasColumnType("varchar"); + .HasColumnType("text"); b.Property("NotificationStatus") .IsRequired() .HasMaxLength(100) - .HasColumnType("varchar"); + .HasColumnType("text"); - b.Property("NotificationType") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("varchar"); + b.Property("NotificationType") + .HasColumnType("integer"); b.Property("UpdatedAt") - .HasColumnType("datetime2"); + .HasColumnType("timestamp with time zone"); b.Property("UpdatedBy") .HasMaxLength(100) - .HasColumnType("varchar"); + .HasColumnType("text"); + + b.Property("UpdatedByTimezoneId") + .HasMaxLength(100) + .HasColumnType("text"); b.HasKey("Id"); @@ -70,40 +77,49 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("int"); + .HasColumnType("integer"); - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("CreatedAt") - .HasColumnType("datetime2"); + .HasColumnType("timestamp with time zone"); b.Property("CreatedBy") .HasMaxLength(100) - .HasColumnType("varchar"); + .HasColumnType("text"); + + b.Property("CreatedByTimezoneId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); b.Property("Description") .IsRequired() .HasMaxLength(255) - .HasColumnType("varchar"); + .HasColumnType("text"); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("varchar"); + .HasColumnType("text"); b.Property("OrderId") - .HasColumnType("int"); + .HasColumnType("integer"); b.Property("UpdatedAt") - .HasColumnType("datetime2"); + .HasColumnType("timestamp with time zone"); b.Property("UpdatedBy") .HasMaxLength(100) - .HasColumnType("varchar"); + .HasColumnType("text"); + + b.Property("UpdatedByTimezoneId") + .HasMaxLength(100) + .HasColumnType("text"); b.Property("Value") .HasPrecision(18, 2) - .HasColumnType("decimal(18,2)"); + .HasColumnType("numeric(18,2)"); b.HasKey("Id"); @@ -116,32 +132,41 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("int"); + .HasColumnType("integer"); - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("CreatedAt") - .HasColumnType("datetime2"); + .HasColumnType("timestamp with time zone"); b.Property("CreatedBy") .HasMaxLength(100) - .HasColumnType("varchar"); + .HasColumnType("text"); + + b.Property("CreatedByTimezoneId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); b.Property("Description") .IsRequired() .HasMaxLength(255) - .HasColumnType("varchar"); + .HasColumnType("text"); b.Property("Total") .HasPrecision(18, 2) - .HasColumnType("decimal(18,2)"); + .HasColumnType("numeric(18,2)"); b.Property("UpdatedAt") - .HasColumnType("datetime2"); + .HasColumnType("timestamp with time zone"); b.Property("UpdatedBy") .HasMaxLength(100) - .HasColumnType("varchar"); + .HasColumnType("text"); + + b.Property("UpdatedByTimezoneId") + .HasMaxLength(100) + .HasColumnType("text"); b.HasKey("Id"); diff --git a/templates/Full/src/Infrastructure/Data/Migrations/20260403110832_CreateTables.cs b/templates/Full/src/Infrastructure/Data/Migrations/20260403110832_CreateTables.cs new file mode 100644 index 00000000..e94e045a --- /dev/null +++ b/templates/Full/src/Infrastructure/Data/Migrations/20260403110832_CreateTables.cs @@ -0,0 +1,102 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Data.Migrations +{ + /// + public partial class CreateTables : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Notification", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + NotificationType = table.Column(type: "integer", nullable: false), + NotificationStatus = table.Column(type: "text", maxLength: 100, nullable: false), + Message = table.Column(type: "text", maxLength: 4000, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedBy = table.Column(type: "text", maxLength: 100, nullable: true), + CreatedByTimezoneId = table.Column(type: "text", maxLength: 100, nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedBy = table.Column(type: "text", maxLength: 100, nullable: true), + UpdatedByTimezoneId = table.Column(type: "text", maxLength: 100, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Notification", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Order", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Description = table.Column(type: "text", maxLength: 255, nullable: false), + Total = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedBy = table.Column(type: "text", maxLength: 100, nullable: true), + CreatedByTimezoneId = table.Column(type: "text", maxLength: 100, nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedBy = table.Column(type: "text", maxLength: 100, nullable: true), + UpdatedByTimezoneId = table.Column(type: "text", maxLength: 100, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Order", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Item", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", maxLength: 200, nullable: false), + Description = table.Column(type: "text", maxLength: 255, nullable: false), + Value = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + OrderId = table.Column(type: "integer", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedBy = table.Column(type: "text", maxLength: 100, nullable: true), + CreatedByTimezoneId = table.Column(type: "text", maxLength: 100, nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedBy = table.Column(type: "text", maxLength: 100, nullable: true), + UpdatedByTimezoneId = table.Column(type: "text", maxLength: 100, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Item", x => x.Id); + table.ForeignKey( + name: "FK_Item_Order_OrderId", + column: x => x.OrderId, + principalTable: "Order", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Item_OrderId", + table: "Item", + column: "OrderId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Item"); + + migrationBuilder.DropTable( + name: "Notification"); + + migrationBuilder.DropTable( + name: "Order"); + } + } +} diff --git a/templates/src/Infrastructure/Data/Migrations/MyDbContextModelSnapshot.cs b/templates/Full/src/Infrastructure/Data/Migrations/MyDbContextModelSnapshot.cs similarity index 57% rename from templates/src/Infrastructure/Data/Migrations/MyDbContextModelSnapshot.cs rename to templates/Full/src/Infrastructure/Data/Migrations/MyDbContextModelSnapshot.cs index aef3491b..c5412c4c 100644 --- a/templates/src/Infrastructure/Data/Migrations/MyDbContextModelSnapshot.cs +++ b/templates/Full/src/Infrastructure/Data/Migrations/MyDbContextModelSnapshot.cs @@ -3,8 +3,8 @@ using Infrastructure.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable @@ -17,46 +17,53 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.8") - .HasAnnotation("Relational:MaxIdentifierLength", 128); + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("Domain.Notifications.Notification", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("int"); + .HasColumnType("integer"); - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("CreatedAt") - .HasColumnType("datetime2"); + .HasColumnType("timestamp with time zone"); b.Property("CreatedBy") .HasMaxLength(100) - .HasColumnType("varchar"); + .HasColumnType("text"); + + b.Property("CreatedByTimezoneId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); b.Property("Message") .HasMaxLength(4000) - .HasColumnType("varchar"); + .HasColumnType("text"); b.Property("NotificationStatus") .IsRequired() .HasMaxLength(100) - .HasColumnType("varchar"); + .HasColumnType("text"); - b.Property("NotificationType") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("varchar"); + b.Property("NotificationType") + .HasColumnType("integer"); b.Property("UpdatedAt") - .HasColumnType("datetime2"); + .HasColumnType("timestamp with time zone"); b.Property("UpdatedBy") .HasMaxLength(100) - .HasColumnType("varchar"); + .HasColumnType("text"); + + b.Property("UpdatedByTimezoneId") + .HasMaxLength(100) + .HasColumnType("text"); b.HasKey("Id"); @@ -67,40 +74,49 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("int"); + .HasColumnType("integer"); - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("CreatedAt") - .HasColumnType("datetime2"); + .HasColumnType("timestamp with time zone"); b.Property("CreatedBy") .HasMaxLength(100) - .HasColumnType("varchar"); + .HasColumnType("text"); + + b.Property("CreatedByTimezoneId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); b.Property("Description") .IsRequired() .HasMaxLength(255) - .HasColumnType("varchar"); + .HasColumnType("text"); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("varchar"); + .HasColumnType("text"); b.Property("OrderId") - .HasColumnType("int"); + .HasColumnType("integer"); b.Property("UpdatedAt") - .HasColumnType("datetime2"); + .HasColumnType("timestamp with time zone"); b.Property("UpdatedBy") .HasMaxLength(100) - .HasColumnType("varchar"); + .HasColumnType("text"); + + b.Property("UpdatedByTimezoneId") + .HasMaxLength(100) + .HasColumnType("text"); b.Property("Value") .HasPrecision(18, 2) - .HasColumnType("decimal(18,2)"); + .HasColumnType("numeric(18,2)"); b.HasKey("Id"); @@ -113,32 +129,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("int"); + .HasColumnType("integer"); - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("CreatedAt") - .HasColumnType("datetime2"); + .HasColumnType("timestamp with time zone"); b.Property("CreatedBy") .HasMaxLength(100) - .HasColumnType("varchar"); + .HasColumnType("text"); + + b.Property("CreatedByTimezoneId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); b.Property("Description") .IsRequired() .HasMaxLength(255) - .HasColumnType("varchar"); + .HasColumnType("text"); b.Property("Total") .HasPrecision(18, 2) - .HasColumnType("decimal(18,2)"); + .HasColumnType("numeric(18,2)"); b.Property("UpdatedAt") - .HasColumnType("datetime2"); + .HasColumnType("timestamp with time zone"); b.Property("UpdatedBy") .HasMaxLength(100) - .HasColumnType("varchar"); + .HasColumnType("text"); + + b.Property("UpdatedByTimezoneId") + .HasMaxLength(100) + .HasColumnType("text"); b.HasKey("Id"); diff --git a/templates/src/Infrastructure/Data/MyDbContext.cs b/templates/Full/src/Infrastructure/Data/MyDbContext.cs similarity index 89% rename from templates/src/Infrastructure/Data/MyDbContext.cs rename to templates/Full/src/Infrastructure/Data/MyDbContext.cs index c839710e..0b978f1a 100644 --- a/templates/src/Infrastructure/Data/MyDbContext.cs +++ b/templates/Full/src/Infrastructure/Data/MyDbContext.cs @@ -14,7 +14,7 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura { configurationBuilder .Properties() - .HaveColumnType("varchar") + .HaveColumnType("text") .HaveMaxLength(100); configurationBuilder @@ -27,6 +27,6 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura configurationBuilder .Properties() - .HaveColumnType("datetime2"); + .HaveColumnType("timestamp with time zone"); } } diff --git a/templates/src/Infrastructure/Data/MyDbContextFactory.cs b/templates/Full/src/Infrastructure/Data/MyDbContextFactory.cs similarity index 81% rename from templates/src/Infrastructure/Data/MyDbContextFactory.cs rename to templates/Full/src/Infrastructure/Data/MyDbContextFactory.cs index 270434b9..e608ad61 100644 --- a/templates/src/Infrastructure/Data/MyDbContextFactory.cs +++ b/templates/Full/src/Infrastructure/Data/MyDbContextFactory.cs @@ -15,7 +15,7 @@ public MyDbContext CreateDbContext(string[] args) { var builder = new DbContextOptionsBuilder(); - builder.UseSqlServer("Server=127.0.0.1,1433;Database=OrderDb;User Id=sa;Password=yourStrong(!)Password;TrustServerCertificate=true;"); + builder.UseNpgsql("Host=127.0.0.1;Port=5432;Database=OrderDb;Username=postgres;Password=yourStrong(!)Password"); return new MyDbContext(builder.Options); } diff --git a/templates/src/Infrastructure/Infrastructure.csproj b/templates/Full/src/Infrastructure/Infrastructure.csproj similarity index 78% rename from templates/src/Infrastructure/Infrastructure.csproj rename to templates/Full/src/Infrastructure/Infrastructure.csproj index 30f85b68..a802f246 100644 --- a/templates/src/Infrastructure/Infrastructure.csproj +++ b/templates/Full/src/Infrastructure/Infrastructure.csproj @@ -3,26 +3,28 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive all - + + + - - - + + + + + - \ No newline at end of file diff --git a/templates/src/Infrastructure/InfrastructureDependencyInjection.cs b/templates/Full/src/Infrastructure/InfrastructureDependencyInjection.cs similarity index 93% rename from templates/src/Infrastructure/InfrastructureDependencyInjection.cs rename to templates/Full/src/Infrastructure/InfrastructureDependencyInjection.cs index 40f1fb7b..cbd83ae4 100644 --- a/templates/src/Infrastructure/InfrastructureDependencyInjection.cs +++ b/templates/Full/src/Infrastructure/InfrastructureDependencyInjection.cs @@ -1,6 +1,5 @@ using Infrastructure.Data; using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; using Infrastructure.OpenTelemetry; using Infrastructure.Cache; using Infrastructure.Messaging; diff --git a/templates/src/Infrastructure/Messaging/Consumers/BaseConsumer.cs b/templates/Full/src/Infrastructure/Messaging/Consumers/BaseConsumer.cs similarity index 60% rename from templates/src/Infrastructure/Messaging/Consumers/BaseConsumer.cs rename to templates/Full/src/Infrastructure/Messaging/Consumers/BaseConsumer.cs index e5471074..72de2699 100644 --- a/templates/src/Infrastructure/Messaging/Consumers/BaseConsumer.cs +++ b/templates/Full/src/Infrastructure/Messaging/Consumers/BaseConsumer.cs @@ -1,13 +1,17 @@ using System.Diagnostics; +using System.Diagnostics.Metrics; using System.Text.Json; using Application.Common.Messages; using Application.Common.Services; +using Domain.Common; +using Domain.Common.Enums; using Infrastructure.Common; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using RabbitMQ.Client; using RabbitMQ.Client.Events; +using Logs = Application.Common.Helpers.Logs; namespace Infrastructure.Messaging.Consumers; @@ -15,18 +19,20 @@ namespace Infrastructure.Messaging.Consumers; internal abstract class BaseConsumer : BaseBackgroundService> where TMessage : BaseMessage { - private readonly string _className = typeof(TConsumer).Name; + private readonly string _consumerName = typeof(TConsumer).Name; private readonly string _queueName; private readonly IDictionary _arguments; private readonly ConnectionFactory _factory; - private readonly Stopwatch _stopwatch = new(); protected IProduceService producerService = null!; + private readonly ActivitySource _activities = DefaultConfigurations.ActivitySource; + protected Counter ConsumerErrorMetric { get; } + protected Counter ConsumerDuplicatedMessageMetric { get; } public BaseConsumer( ILogger> logger, IServiceScopeFactory serviceScopeFactory, IConfiguration configuration, - string queueName, + NotificationType queueName, IDictionary arguments = null! ) : base(logger, serviceScopeFactory, configuration) { @@ -37,28 +43,36 @@ public BaseConsumer( throw new ArgumentException("Invalid RabbitMQ connection string."); } - _queueName = queueName; + _queueName = queueName.ToString(); _arguments = arguments; _factory = new() { Uri = new(connectionString) }; + + ConsumerErrorMetric = DefaultConfigurations.Meter + .CreateCounter($"{_consumerName}.Error", "total", "Number of times the consumer encountered an error"); + + ConsumerDuplicatedMessageMetric = DefaultConfigurations.Meter + .CreateCounter($"{_consumerName}.DuplicatedMessage", "total", "Number of times the consumer received a duplicated message"); } protected override async Task ExecuteInternalAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) => await HandleRabbitMqAsync( async (message, cancellationToken) => { + var messageType = typeof(TMessage).Name; + using var activity = _activities.StartActivity($"{_consumerName}", ActivityKind.Consumer); + activity?.SetTag("correlationId", message.CorrelationId); + activity?.SetTag("queueName", _queueName); + producerService = serviceProvider.GetRequiredService(); + try { - _stopwatch.Restart(); var hybridCacheService = serviceProvider.GetRequiredService(); + Logs.Debug(logger, message.CorrelationId, messageType + " received. Checking if it has already been processed."); - logger.LogInformation( - "[{ClassName}] | [HandleMessageAsync] | CorrelationId: {CorrelationId} | Received message: {MessageType}", - _className, message.CorrelationId, typeof(TMessage).Name - ); - - var isExecutedKey = _className + "-" + message.CorrelationId; + var isExecutedKey = _consumerName + "-" + message.CorrelationId; var isExecuted = await hybridCacheService.GetOrCreateAsync( + message.CorrelationId, isExecutedKey, async (cancellationToken) => false, cancellationToken @@ -66,33 +80,24 @@ protected override async Task ExecuteInternalAsync(IServiceProvider serviceProvi if (isExecuted) { - logger.LogWarning( - "[{ClassName}] | [HandleMessageAsync] | CorrelationId: {CorrelationId} | Duplicate message detected. Skipping processing.", - _className, message.CorrelationId - ); + Logs.Warning(logger, message.CorrelationId, messageType + " has already been processed. Skipping."); + ConsumerDuplicatedMessageMetric.Add(1); return; } - logger.LogInformation( - "[{ClassName}] | [HandleMessageAsync] | CorrelationId: {CorrelationId} | Start processing message.", - _className, message.CorrelationId - ); + Logs.DebugStartingOperation(logger, message.CorrelationId, messageType + " processing started."); await HandleUseCaseAsync(serviceProvider, message, cancellationToken); - await hybridCacheService.CreateAsync(isExecutedKey, true, cancellationToken); + await hybridCacheService.CreateAsync(message.CorrelationId, isExecutedKey, true, cancellationToken); - logger.LogInformation( - "[{ClassName}] | [HandleMessageAsync] | CorrelationId: {CorrelationId} | Processed message in {ElapsedMilliseconds} ms", - _className, message.CorrelationId, _stopwatch.ElapsedMilliseconds - ); + Logs.DebugFinishedOperation(logger, message.CorrelationId, messageType + " processing finished."); } catch (Exception ex) { - logger.LogError( - "[{ClassName}] | [HandleMessageAsync] | CorrelationId: {CorrelationId} | Error processing message: {ErrorMessage} | StackTrace: {StackTrace}", - _className, message?.CorrelationId, ex.Message, ex.StackTrace - ); + Logs.Error(logger, message.CorrelationId, ex.Message); + + ConsumerErrorMetric.Add(1); _ = producerService.HandleAsync(message!, CancellationToken.None, _queueName + "_deadLetter"); @@ -110,6 +115,8 @@ CancellationToken cancellationToken var connection = await _factory.CreateConnectionAsync(cancellationToken); var channel = await connection.CreateChannelAsync(cancellationToken: cancellationToken); + Logs.Debug(logger, Guid.NewGuid(), "Connected to RabbitMQ. Declaring queues."); + await channel.QueueDeclareAsync( queue: _queueName, exclusive: false, @@ -128,6 +135,8 @@ await channel.QueueDeclareAsync( AsyncEventingBasicConsumer consumer = new(channel); + Logs.Debug(logger, Guid.NewGuid(), "Queues declared. Starting to consume messages."); + consumer.ReceivedAsync += async (model, eventArguments) => { var basicProperties = eventArguments.BasicProperties; @@ -136,37 +145,36 @@ await channel.QueueDeclareAsync( TMessage message = null!; try { + Logs.Debug(logger, Guid.NewGuid(), "Message received. Deserializing."); + message = JsonSerializer.Deserialize(body)!; + Logs.Debug(logger, message.CorrelationId, "Message deserialized. Validating."); + if (message == null || message.GetType() != typeof(TMessage)) { - logger.LogWarning( - "[{ClassName}] | [HandleMessageAsync] | Received null message of type {MessageType}", - _className, typeof(TMessage).Name - ); + Logs.Warning(logger, Guid.NewGuid(), typeof(TMessage).Name + " is null or of incorrect type."); return; } } catch (JsonException ex) { - logger.LogError( - "[{ClassName}] | [HandleRabbitMqAsync] | [{CorrelationId}] | AppId: {AppId} | ClusterId: {ClusterId} | Error deserializing message: {ErrorMessage} | StackTrace: {StackTrace}", - _className, basicProperties.CorrelationId, basicProperties.AppId, basicProperties.ClusterId, ex.Message, ex.StackTrace - ); + Logs.Error(logger, Guid.NewGuid(), ex.Message); throw; } catch (Exception ex) { - logger.LogError( - "[{ClassName}] | [HandleRabbitMqAsync] | [{CorrelationId}] | AppId: {AppId} | ClusterId: {ClusterId} | Unexpected error: {ErrorMessage} | StackTrace: {StackTrace}", - _className, basicProperties.CorrelationId, basicProperties.AppId, basicProperties.ClusterId, ex.Message, ex.StackTrace - ); + Logs.Error(logger, Guid.NewGuid(), ex.Message); throw; } + Logs.Debug(logger, message.CorrelationId, "Message validated. Handling use case."); + await handleAsync.Invoke(message, cancellationToken); + + Logs.Debug(logger, message.CorrelationId, "Use case handled."); }; await channel.BasicConsumeAsync( diff --git a/templates/src/Infrastructure/Messaging/Consumers/CreateNotificationConsumer.cs b/templates/Full/src/Infrastructure/Messaging/Consumers/CreateNotificationConsumer.cs similarity index 96% rename from templates/src/Infrastructure/Messaging/Consumers/CreateNotificationConsumer.cs rename to templates/Full/src/Infrastructure/Messaging/Consumers/CreateNotificationConsumer.cs index 776808bb..7745f8b8 100644 --- a/templates/src/Infrastructure/Messaging/Consumers/CreateNotificationConsumer.cs +++ b/templates/Full/src/Infrastructure/Messaging/Consumers/CreateNotificationConsumer.cs @@ -1,7 +1,7 @@ -using Application.Common.Constants; using Application.Common.Messages; using Application.Common.UseCases; using Application.Notifications; +using Domain.Common.Enums; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/templates/src/Infrastructure/Messaging/MessagingDependencyInjection.cs b/templates/Full/src/Infrastructure/Messaging/MessagingDependencyInjection.cs similarity index 100% rename from templates/src/Infrastructure/Messaging/MessagingDependencyInjection.cs rename to templates/Full/src/Infrastructure/Messaging/MessagingDependencyInjection.cs diff --git a/templates/src/Infrastructure/Messaging/Producers/ProducerService.cs b/templates/Full/src/Infrastructure/Messaging/Producers/ProducerService.cs similarity index 64% rename from templates/src/Infrastructure/Messaging/Producers/ProducerService.cs rename to templates/Full/src/Infrastructure/Messaging/Producers/ProducerService.cs index 1170fa3e..dca50bb8 100644 --- a/templates/src/Infrastructure/Messaging/Producers/ProducerService.cs +++ b/templates/Full/src/Infrastructure/Messaging/Producers/ProducerService.cs @@ -1,19 +1,20 @@ -using System.Diagnostics; using System.Text.Json; using Application.Common.Messages; using Application.Common.Services; +using Application.Common.Helpers; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using RabbitMQ.Client; +using System.Diagnostics; +using Domain.Common; namespace Infrastructure.Messaging.Producers; public sealed class ProducerService : IProduceService { - private readonly string _className = nameof(ProducerService); private readonly ILogger _logger; private readonly ConnectionFactory _factory; - private readonly Stopwatch _stopWatch = new(); + private readonly ActivitySource _activities = DefaultConfigurations.ActivitySource; public ProducerService(ILogger logger, IConfiguration configuration) { @@ -38,15 +39,12 @@ public async Task HandleAsync( { await Task.Yield(); - _stopWatch.Restart(); + using var activity = _activities.StartActivity($"{nameof(ProducerService)}.{nameof(HandleAsync)}.{typeof(TMessage).Name}"); using var connection = await _factory.CreateConnectionAsync(cancellationToken); using var channel = await connection.CreateChannelAsync(cancellationToken: cancellationToken); - _logger.LogInformation( - "[{ClassName}] | [HandleAsync] | CorrelationId: {CorrelationId} | Publishing message: {MessageType}", - _className, message.CorrelationId, typeof(TMessage).Name - ); + Logs.DebugStartingOperation(_logger, message.CorrelationId, typeof(TMessage).Name + " publishing started."); await channel.BasicPublishAsync( exchange: exchange, @@ -55,10 +53,7 @@ await channel.BasicPublishAsync( cancellationToken: cancellationToken ); - _logger.LogInformation( - "[{ClassName}] | [HandleAsync] | CorrelationId: {CorrelationId} | Message published: {MessageType} | Elapsed time: {ElapsedMilliseconds} ms", - _className, message.CorrelationId, typeof(TMessage).Name, _stopWatch.ElapsedMilliseconds - ); + Logs.DebugFinishedOperation(_logger, message.CorrelationId, typeof(TMessage).Name + " published."); } public async Task HandleAsync( @@ -70,17 +65,17 @@ public async Task HandleAsync( { await Task.Yield(); - _stopWatch.Restart(); + using var activity = _activities.StartActivity($"{nameof(ProducerService)}.{nameof(HandleAsync)}.{typeof(TMessage).Name}.Batch"); + + Logs.Debug(_logger, messages.FirstOrDefault()?.CorrelationId ?? Guid.Empty, typeof(TMessage).Name + " batch publishing started."); using var connection = await _factory.CreateConnectionAsync(cancellationToken); using var channel = await connection.CreateChannelAsync(cancellationToken: cancellationToken); - _logger.LogInformation( - "[{ClassName}] | [HandleAsync] | CorrelationId: {CorrelationId} | Publishing batch of messages: {MessageType}", - _className, messages.Select(m => m.CorrelationId), typeof(TMessage).Name - ); - foreach (var message in messages) + { + Logs.DebugStartingOperation(_logger, message.CorrelationId, typeof(TMessage).Name + " batch publishing started."); + await channel.BasicPublishAsync( exchange: exchange, routingKey: queue, @@ -88,9 +83,9 @@ await channel.BasicPublishAsync( cancellationToken: cancellationToken ); - _logger.LogInformation( - "[{ClassName}] | [HandleAsync] | CorrelationId: {CorrelationId} | Batch of messages published: {MessageType} | Elapsed time: {ElapsedMilliseconds} ms", - _className, messages.Select(m => m.CorrelationId), typeof(TMessage).Name, _stopWatch.ElapsedMilliseconds - ); + Logs.DebugFinishedOperation(_logger, message.CorrelationId, typeof(TMessage).Name + " batch published."); + } + + Logs.Debug(_logger, messages.FirstOrDefault()?.CorrelationId ?? Guid.Empty, typeof(TMessage).Name + " batch publishing finished."); } } diff --git a/templates/Full/src/Infrastructure/OpenTelemetry/InfrastructureOpenTelemetryDependencyInjection.cs b/templates/Full/src/Infrastructure/OpenTelemetry/InfrastructureOpenTelemetryDependencyInjection.cs new file mode 100644 index 00000000..047d5dc1 --- /dev/null +++ b/templates/Full/src/Infrastructure/OpenTelemetry/InfrastructureOpenTelemetryDependencyInjection.cs @@ -0,0 +1,63 @@ +using Domain.Common; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace Infrastructure.OpenTelemetry; + +internal static class InfrastructureOpenTelemetryDependencyInjection +{ + extension(WebApplicationBuilder builder) + { + public 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() + .AddRabbitMQInstrumentation() + .AddGrpcClientInstrumentation() + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddEntityFrameworkCoreInstrumentation() + .AddOtlpExporter() + ); + + builder.Logging.AddOpenTelemetry(options => + { + options.IncludeFormattedMessage = true; + options.IncludeScopes = true; + options.ParseStateValues = true; + options + .SetResourceBuilder(resourceBuilder) + .AttachLogsToActivityEvent() + .AddOtlpExporter(); + }); + + return builder; + } + } +} diff --git a/templates/Full/src/Infrastructure/packages.lock.json b/templates/Full/src/Infrastructure/packages.lock.json new file mode 100644 index 00000000..eb62228c --- /dev/null +++ b/templates/Full/src/Infrastructure/packages.lock.json @@ -0,0 +1,606 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "Microsoft.EntityFrameworkCore.Design": { + "type": "Direct", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "gm6f0cC2w/2tcd4GeZJqEMruTercpIJfO5sSAFLtqTqblDBHgAFk70xwshUIUVX4I6sZwdEUSd1YxoKFk1AL0w==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "18.0.2", + "Microsoft.CodeAnalysis.CSharp": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", + "Microsoft.CodeAnalysis.Workspaces.MSBuild": "5.0.0", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Mono.TextTemplating": "3.0.0", + "Newtonsoft.Json": "13.0.3" + } + }, + "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" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "Direct", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" + } + }, + "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.EntityFrameworkCore": { + "type": "Direct", + "requested": "[1.15.0-beta.1, )", + "resolved": "1.15.0-beta.1", + "contentHash": "N01GzP+r8lpSBiqjRX0/WjSp17r+zk6dKvGKthiASyFzF44lrJo8cA3ihXnw3p4Rnqg1mVjOYy19R6iJ84NTpg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "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" + } + }, + "RabbitMQ.Client": { + "type": "Direct", + "requested": "[7.2.1, )", + "resolved": "7.2.1", + "contentHash": "YKXEfg9fVQiTKgZlvIhAfPSFaamEgi8DsQmisCH0IAsU4FYLrtoguDrDj6JtJVGUt40QPnBLRH6fTQcAC4qsOg==", + "dependencies": { + "System.Threading.RateLimiting": "8.0.0" + } + }, + "RabbitMQ.Client.OpenTelemetry": { + "type": "Direct", + "requested": "[1.0.0-rc.2, )", + "resolved": "1.0.0-rc.2", + "contentHash": "fVjEZ8DsLDw3EEp/Q5XGFhk7Rvluh32n+38kyBcCdZu/F7cOuj6ETo+pYMhXHtcKGk856sEEOaEWR+alsETv9w==", + "dependencies": { + "OpenTelemetry.Api": "1.9.0", + "RabbitMQ.Client": "7.2.0" + } + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Microsoft.Build.Framework": { + "type": "Transitive", + "resolved": "18.0.2", + "contentHash": "sOSb+0J4G/jCBW/YqmRuL0eOMXgfw1KQLdC9TkbvfA5xs7uNm+PBQXJCOzSJGXtZcZrtXozcwxPmUiRUbmd7FA==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZXRAdvH6GiDeHRyd3q/km8Z44RoM6FBWHd+gen/la81mVnAdHTEsEkO5J0TCNXBymAcx5UYKt5TvgKBhaLJEow==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5DSyJ9bk+ATuDy7fp2Zt0mJStDVKbBoiz1DyfAwSa+k4H4IwykAUcV3URelw5b8/iVbfSaOwkwmPUZH6opZKCw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]" + } + }, + "Microsoft.CodeAnalysis.CSharp.Workspaces": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "Al/Q8B+yO8odSqGVpSvrShMFDvlQdIBU//F3E6Rb0YdiLSALE9wh/pvozPNnfmh5HDnvU+mkmSjpz4hQO++jaA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.CSharp": "[5.0.0]", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZbUmIvT6lqTNKiv06Jl5wf0MTMi1vQ1oH7ou4CLcs2C/no/L7EhP3T8y3XXvn9VbqMcJaJnEsNA1jwYUMgc5jg==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.MSBuild": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "/G+LVoAGMz6Ae8nm+PGLxSw+F5RjYx/J7irbTO5uKAPw1bxHyQJLc/YOnpDxt+EpPtYxvC9wvBsg/kETZp1F9Q==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "17.11.31", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "Microsoft.Extensions.DependencyInjection": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0", + "Microsoft.VisualStudio.SolutionPersistence": "1.0.52", + "Newtonsoft.Json": "13.0.3", + "System.Composition": "9.0.0" + } + }, + "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.Configuration": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "H4SWETCh/cC5L1WtWchHR6LntGk3rDTTznZMssr4cL8IbDmMWBxY+MOGDc/ASnqNolLKPIWHWeuC1ddiL/iNPw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", + "Microsoft.Extensions.Primitives": "10.0.0" + } + }, + "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.0", + "contentHash": "tMF9wNh+hlyYDWB8mrFCQHQmWHlRosol1b/N2Jrefy1bFLnuTlgSYmPyHNmz8xVQgs7DpXytBRWxGhG+mSTp0g==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.0" + } + }, + "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.Abstractions": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0" + } + }, + "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.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.0", + "contentHash": "tL9cSl3maS5FPzp/3MtlZI21ExWhni0nnUCF8HY4npTsINw45n9SNDbkKXBMtFyUFGSsQep25fHIDN4f/Vp3AQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", + "Microsoft.Extensions.Configuration.Binder": "10.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "Microsoft.Extensions.Primitives": "10.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" + }, + "Microsoft.VisualStudio.SolutionPersistence": { + "type": "Transitive", + "resolved": "1.0.52", + "contentHash": "oNv2JtYXhpdJrX63nibx1JT3uCESOBQ1LAk7Dtz/sr0+laW0KRM6eKp4CZ3MHDR2siIkKsY8MmUkeP5DKkQQ5w==" + }, + "Mono.TextTemplating": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "Npgsql": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "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==" + }, + "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.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Composition": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "3Djj70fFTraOarSKmRnmRy/zm4YurICm+kiCtI0dYRqGJnLX6nJ+G3WYuFJ173cAPax/gh96REcbNiVqcrypFQ==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Convention": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0", + "System.Composition.TypedParts": "9.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "iri00l/zIX9g4lHMY+Nz0qV1n40+jFYAmgsaiNn16xvt2RDwlqByNG4wgblagnDYxm3YSQQ0jLlC/7Xlk9CzyA==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "+vuqVP6xpi582XIjJi6OCsIxuoTZfR0M7WWufk3uGDeCl3wGW6KnpylUJ3iiXdPByPE0vR5TjJgR6hDLez4FQg==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "OFqSeFeJYr7kHxDfaViGM1ymk7d4JxK//VSoNF9Ux0gpqkLsauDZpu89kTHHNdCWfSljbFcvAafGyBoY094btQ==", + "dependencies": { + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "w1HOlQY1zsOWYussjFGZCEYF2UZXgvoYnS94NIu2CBnAGMbXFAX8PY8c92KwUItPmowal68jnVLBCzdrWLeEKA==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "aRZlojCCGEHDKqh43jaDgaVpYETsgd7Nx4g1zwLKMtv4iTo0627715ajEFNpEEBTgLmvZuv8K0EVxc3sM4NWJA==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "application": { + "type": "Project", + "dependencies": { + "Domain": "[1.0.0, )", + "FluentValidation": "[12.1.1, )", + "FluentValidation.DependencyInjectionExtensions": "[12.1.1, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.Extensions.Logging": "[10.0.5, )" + } + }, + "domain": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "[10.0.5, )" + } + }, + "FluentValidation": { + "type": "CentralTransitive", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" + }, + "FluentValidation.DependencyInjectionExtensions": { + "type": "CentralTransitive", + "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": "CentralTransitive", + "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.EntityFrameworkCore.Relational": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "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" + } + }, + "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==" + } + } + } +} \ No newline at end of file diff --git a/templates/src/WebApp/Endpoints/EndpointExtensions.cs b/templates/Full/src/WebApp/Endpoints/EndpointExtensions.cs similarity index 100% rename from templates/src/WebApp/Endpoints/EndpointExtensions.cs rename to templates/Full/src/WebApp/Endpoints/EndpointExtensions.cs diff --git a/templates/src/WebApp/Endpoints/OrderEndpoints.cs b/templates/Full/src/WebApp/Endpoints/OrderEndpoints.cs similarity index 98% rename from templates/src/WebApp/Endpoints/OrderEndpoints.cs rename to templates/Full/src/WebApp/Endpoints/OrderEndpoints.cs index 4c3ab0a7..0aa915a1 100644 --- a/templates/src/WebApp/Endpoints/OrderEndpoints.cs +++ b/templates/Full/src/WebApp/Endpoints/OrderEndpoints.cs @@ -25,6 +25,7 @@ public static WebApplication MapOrderEndpoints(this WebApplication app) var response = cacheEnabled switch { true => await cache.GetOrCreateAsync( + correlationId, $"{nameof(OrderEndpoints)}-{id}", async (cancellationToken) => await useCase.HandleAsync(new(correlationId, id), cancellationToken), cancellationToken diff --git a/templates/src/WebApp/HealthChecks/HealthCheckExtensions.cs b/templates/Full/src/WebApp/Extensions/HealthCheckExtensions.cs similarity index 95% rename from templates/src/WebApp/HealthChecks/HealthCheckExtensions.cs rename to templates/Full/src/WebApp/Extensions/HealthCheckExtensions.cs index f31395ff..e52f312e 100644 --- a/templates/src/WebApp/HealthChecks/HealthCheckExtensions.cs +++ b/templates/Full/src/WebApp/Extensions/HealthCheckExtensions.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using RabbitMQ.Client; -namespace WebApp.HealthChecks; +namespace WebApp.Extensions; internal static class HealthCheckExtensions { @@ -23,9 +23,9 @@ IConfiguration configuration }) .AddHealthChecks() .AddCheck("self", () => HealthCheckResult.Healthy()) - .AddSqlServer( + .AddNpgSql( configuration.GetConnectionString("OrderDb")!, - name: "SqlServer", + name: "PostgreSQL", tags: ["services"] ) .AddRedis( diff --git a/templates/src/WebApp/GrpcServices/GrpcServiceExtensions.cs b/templates/Full/src/WebApp/GrpcServices/GrpcServiceExtensions.cs similarity index 100% rename from templates/src/WebApp/GrpcServices/GrpcServiceExtensions.cs rename to templates/Full/src/WebApp/GrpcServices/GrpcServiceExtensions.cs diff --git a/templates/src/WebApp/GrpcServices/OrderService.cs b/templates/Full/src/WebApp/GrpcServices/OrderService.cs similarity index 94% rename from templates/src/WebApp/GrpcServices/OrderService.cs rename to templates/Full/src/WebApp/GrpcServices/OrderService.cs index 7430d007..f63b3a90 100644 --- a/templates/src/WebApp/GrpcServices/OrderService.cs +++ b/templates/Full/src/WebApp/GrpcServices/OrderService.cs @@ -23,11 +23,12 @@ public override async Task Get( ServerCallContext context ) { + var correlationId = Guid.TryParse(request.CorrelationId, out var guid) ? guid : Guid.Empty; var response = await _cache.GetOrCreateAsync( + correlationId, $"{nameof(OrderService)}-{request.Id}", async cancellationToken => { - var correlationId = Guid.TryParse(request.CorrelationId, out var guid) ? guid : Guid.Empty; return await _useCase.HandleAsync(new(correlationId, request.Id), cancellationToken); }, context.CancellationToken diff --git a/templates/src/WebApp/Middlewares/ExceptionHandlingMiddleware.cs b/templates/Full/src/WebApp/Middlewares/ExceptionHandlingMiddleware.cs similarity index 82% rename from templates/src/WebApp/Middlewares/ExceptionHandlingMiddleware.cs rename to templates/Full/src/WebApp/Middlewares/ExceptionHandlingMiddleware.cs index 99d1fb61..dd8ce652 100644 --- a/templates/src/WebApp/Middlewares/ExceptionHandlingMiddleware.cs +++ b/templates/Full/src/WebApp/Middlewares/ExceptionHandlingMiddleware.cs @@ -1,5 +1,6 @@ using System.Net; using Application.Common.Requests; +using Application.Common.Helpers; namespace WebApp.Middlewares; @@ -7,7 +8,6 @@ internal sealed class ExceptionHandlingMiddleware(RequestDelegate next, ILogger< { private readonly RequestDelegate _next = next; private readonly ILogger _logger = logger; - private readonly string _className = nameof(ExceptionHandlingMiddleware); public async Task InvokeAsync(HttpContext context) { @@ -23,7 +23,7 @@ public async Task InvokeAsync(HttpContext context) private async Task HandleExceptionAsync(HttpContext context, Exception exception) { - _logger.LogError(exception, "[{ClassName}] | [{Method}] | {Message}", _className, nameof(HandleExceptionAsync), exception.Message); + Logs.Error(_logger, exception.Message); BaseResponse response = new(false, exception.Message); diff --git a/templates/src/WebApp/Program.cs b/templates/Full/src/WebApp/Program.cs similarity index 92% rename from templates/src/WebApp/Program.cs rename to templates/Full/src/WebApp/Program.cs index 0ac11494..4b6b5685 100644 --- a/templates/src/WebApp/Program.cs +++ b/templates/Full/src/WebApp/Program.cs @@ -6,8 +6,8 @@ using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Server.Kestrel.Core; using WebApp.Endpoints; +using WebApp.Extensions; using WebApp.GrpcServices; -using WebApp.HealthChecks; using WebApp.Middlewares; namespace WebApp; @@ -22,13 +22,8 @@ private static async Task Main(string[] args) builder.Services.AddEndpointsApiExplorer(); builder.Services.AddGrpc(); - builder.Services.AddCustomHealthChecks(builder.Configuration); - - builder.Services.AddResponseCompression(options => - { - options.EnableForHttps = true; - }); + builder.Services.AddResponseCompression(); builder.Services.Configure(options => { diff --git a/templates/Full/src/WebApp/Properties/launchSettings.json b/templates/Full/src/WebApp/Properties/launchSettings.json new file mode 100644 index 00000000..6b70726a --- /dev/null +++ b/templates/Full/src/WebApp/Properties/launchSettings.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "Development": { + "commandName": "Project", + "dotnetRunMessages": true, + "sqlDebugging": true, + "launchBrowser": false, + "applicationUrl": "https://*:7175;http://*:5010", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "OTEL_SERVICE_NAME": "Hexagonal.Solution.Template.WebApp", + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317", + "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", + "OTEL_RESOURCE_ATTRIBUTES": "service.namespace=development", + "ENABLE_SENSITIVE_DATA_LOGGING": "true" + } + } + } +} diff --git a/templates/src/WebApp/Protos/order.proto b/templates/Full/src/WebApp/Protos/order.proto similarity index 100% rename from templates/src/WebApp/Protos/order.proto rename to templates/Full/src/WebApp/Protos/order.proto diff --git a/templates/src/WebApp/WebApp.csproj b/templates/Full/src/WebApp/WebApp.csproj similarity index 75% rename from templates/src/WebApp/WebApp.csproj rename to templates/Full/src/WebApp/WebApp.csproj index 2a7223cb..077dffdd 100644 --- a/templates/src/WebApp/WebApp.csproj +++ b/templates/Full/src/WebApp/WebApp.csproj @@ -1,10 +1,7 @@  - - false - - + diff --git a/templates/src/WebApp/WebApp.http b/templates/Full/src/WebApp/WebApp.http similarity index 100% rename from templates/src/WebApp/WebApp.http rename to templates/Full/src/WebApp/WebApp.http diff --git a/templates/Full/src/WebApp/appsettings.json b/templates/Full/src/WebApp/appsettings.json new file mode 100644 index 00000000..b3cdd8e8 --- /dev/null +++ b/templates/Full/src/WebApp/appsettings.json @@ -0,0 +1,16 @@ +{ + "ConnectionStrings": { + "OrderDb": "Host=127.0.0.1;Port=5432;Database=OrderDb;Username=postgres;Password=cY5VvZkkh4AzES", + "Redis": "localhost:6379", + "RabbitMq": "amqp://guest:guest@localhost:5672/" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore": "Information", + "Microsoft.EntityFrameworkCore.Database.Command": "Information" + } + } +} \ No newline at end of file diff --git a/templates/Full/src/WebApp/packages.lock.json b/templates/Full/src/WebApp/packages.lock.json new file mode 100644 index 00000000..1950c02f --- /dev/null +++ b/templates/Full/src/WebApp/packages.lock.json @@ -0,0 +1,374 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "AspNetCore.HealthChecks.NpgSql": { + "type": "Direct", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "npc58/AD5zuVxERdhCl2Kb7WnL37mwX42SJcXIwvmEig0/dugOLg3SIwtfvvh3TnvTwR/sk5LYNkkPaBdks61A==", + "dependencies": { + "Npgsql": "8.0.3" + } + }, + "AspNetCore.HealthChecks.Rabbitmq": { + "type": "Direct", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "7WSQ7EwioA5niakzzLtGVcZMEOh+42fSwrI24vnNsT7gZuVGOViNekyz38G6wBPYKcpL/lUkMdg3ZaCiZTi/Dw==", + "dependencies": { + "RabbitMQ.Client": "7.0.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" + } + }, + "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" + } + }, + "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.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==" + }, + "Npgsql": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==" + }, + "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==" + }, + "StackExchange.Redis": { + "type": "Transitive", + "resolved": "2.7.27", + "contentHash": "Uqc2OQHglqj9/FfGQ6RkKFkZfHySfZlfmbCl+hc+u2I/IqunfelQ7QJi7ZhvAJxUtu80pildVX6NPLdDaUffOw==", + "dependencies": { + "Pipelines.Sockets.Unofficial": "2.2.8" + } + }, + "application": { + "type": "Project", + "dependencies": { + "Domain": "[1.0.0, )", + "FluentValidation": "[12.1.1, )", + "FluentValidation.DependencyInjectionExtensions": "[12.1.1, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )" + } + }, + "domain": { + "type": "Project" + }, + "infrastructure": { + "type": "Project", + "dependencies": { + "Application": "[1.0.0, )", + "Microsoft.Extensions.Caching.Hybrid": "[10.4.0, )", + "Microsoft.Extensions.Caching.StackExchangeRedis": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", + "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.EntityFrameworkCore": "[1.15.0-beta.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, )", + "RabbitMQ.Client": "[7.2.1, )", + "RabbitMQ.Client.OpenTelemetry": "[1.0.0-rc.2, )" + } + }, + "FluentValidation": { + "type": "CentralTransitive", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" + }, + "FluentValidation.DependencyInjectionExtensions": { + "type": "CentralTransitive", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "D0VXh4dtjjX2aQizuaa0g6R8X3U1JaVqJPfGCvLwZX9t/O2h7tkpbitbadQMfwcgSPdDbI2vDxuwRMv/Uf9dHA==", + "dependencies": { + "FluentValidation": "12.1.1" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "CentralTransitive", + "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.EntityFrameworkCore.Relational": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.5" + } + }, + "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" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" + } + }, + "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.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[1.15.0-beta.1, )", + "resolved": "1.15.0-beta.1", + "contentHash": "N01GzP+r8lpSBiqjRX0/WjSp17r+zk6dKvGKthiASyFzF44lrJo8cA3ihXnw3p4Rnqg1mVjOYy19R6iJ84NTpg==", + "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" + } + }, + "RabbitMQ.Client": { + "type": "CentralTransitive", + "requested": "[7.2.1, )", + "resolved": "7.2.1", + "contentHash": "YKXEfg9fVQiTKgZlvIhAfPSFaamEgi8DsQmisCH0IAsU4FYLrtoguDrDj6JtJVGUt40QPnBLRH6fTQcAC4qsOg==" + }, + "RabbitMQ.Client.OpenTelemetry": { + "type": "CentralTransitive", + "requested": "[1.0.0-rc.2, )", + "resolved": "1.0.0-rc.2", + "contentHash": "fVjEZ8DsLDw3EEp/Q5XGFhk7Rvluh32n+38kyBcCdZu/F7cOuj6ETo+pYMhXHtcKGk856sEEOaEWR+alsETv9w==", + "dependencies": { + "OpenTelemetry.Api": "1.9.0", + "RabbitMQ.Client": "7.2.0" + } + } + } + } +} \ No newline at end of file diff --git a/templates/Full/tests/CommonTests/CommonTests.csproj b/templates/Full/tests/CommonTests/CommonTests.csproj new file mode 100644 index 00000000..3da95e90 --- /dev/null +++ b/templates/Full/tests/CommonTests/CommonTests.csproj @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/templates/Full/tests/CommonTests/Fixtures/BaseFixture.cs b/templates/Full/tests/CommonTests/Fixtures/BaseFixture.cs new file mode 100644 index 00000000..214514c8 --- /dev/null +++ b/templates/Full/tests/CommonTests/Fixtures/BaseFixture.cs @@ -0,0 +1,16 @@ +using AutoFixture; + +namespace CommonTests.Fixtures; +public class BaseFixture +{ + public Fixture AutoFixture { get; } + + public CancellationToken CancellationToken { get; } + + public BaseFixture() + { + AutoFixture = new Fixture(); + AutoFixture.Behaviors.Add(new OmitOnRecursionBehavior()); + CancellationToken = CancellationToken.None; + } +} diff --git a/templates/Full/tests/CommonTests/packages.lock.json b/templates/Full/tests/CommonTests/packages.lock.json new file mode 100644 index 00000000..7d97c3a9 --- /dev/null +++ b/templates/Full/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/tests/IntegrationTests/Common/CustomWebApplicationFactory.cs b/templates/Full/tests/IntegrationTests/Common/CustomWebApplicationFactory.cs similarity index 85% rename from templates/tests/IntegrationTests/Common/CustomWebApplicationFactory.cs rename to templates/Full/tests/IntegrationTests/Common/CustomWebApplicationFactory.cs index bf0f7415..fb8470ae 100644 --- a/templates/tests/IntegrationTests/Common/CustomWebApplicationFactory.cs +++ b/templates/Full/tests/IntegrationTests/Common/CustomWebApplicationFactory.cs @@ -14,9 +14,9 @@ public sealed class WebApplicationFactoryCollectionDefinition : IClassFixture : WebApplicationFactory, IDisposable where TProgram : class { - protected string? _connectionString = "Server=127.0.0.1,1433;Database=OrderDb;User Id=sa;Password=cY5VvZkkh4AzES;TrustServerCertificate=true;"; + protected string? ConnectionString { get; } = "Host=127.0.0.1;Port=5432;Database=OrderDb;Username=postgres;Password=cY5VvZkkh4AzES"; - public MyDbContext? MyDbContext; + public MyDbContext? MyDbContext { get; set; } public CustomWebApplicationFactory() => SetDbContext(); @@ -34,14 +34,14 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) services.Remove(dbConnectionDescriptor!); - services.AddDbContextFactory((options) => options.UseSqlServer(_connectionString)); + services.AddDbContextFactory((options) => options.UseNpgsql(ConnectionString)); }); } public void SetDbContext() { var contextOptions = new DbContextOptionsBuilder() - .UseSqlServer(_connectionString) + .UseNpgsql(ConnectionString) .Options; MyDbContext = new(contextOptions); @@ -53,6 +53,7 @@ public void SetDbContext() public new void Dispose() { MyDbContext!.Dispose(); + GC.SuppressFinalize(this); // base.Dispose(); } } diff --git a/templates/tests/IntegrationTests/Data/BaseDataFixture.cs b/templates/Full/tests/IntegrationTests/Data/BaseDataFixture.cs similarity index 76% rename from templates/tests/IntegrationTests/Data/BaseDataFixture.cs rename to templates/Full/tests/IntegrationTests/Data/BaseDataFixture.cs index f5be68bb..f186ec92 100644 --- a/templates/tests/IntegrationTests/Data/BaseDataFixture.cs +++ b/templates/Full/tests/IntegrationTests/Data/BaseDataFixture.cs @@ -8,11 +8,11 @@ namespace IntegrationTests.Data; public class BaseDataFixture : BaseFixture { - public required IBaseRepository repository; + public required IBaseRepository Repository { get; set; } public void SetRepository(CustomWebApplicationFactory factory) { var scope = factory.Services.CreateAsyncScope(); - repository = scope.ServiceProvider.GetRequiredService(); + Repository = scope.ServiceProvider.GetRequiredService(); } } diff --git a/templates/tests/IntegrationTests/Data/BaseRepositoryTest.cs b/templates/Full/tests/IntegrationTests/Data/BaseRepositoryTest.cs similarity index 61% rename from templates/tests/IntegrationTests/Data/BaseRepositoryTest.cs rename to templates/Full/tests/IntegrationTests/Data/BaseRepositoryTest.cs index dbe32e54..5a8e5ed9 100644 --- a/templates/tests/IntegrationTests/Data/BaseRepositoryTest.cs +++ b/templates/Full/tests/IntegrationTests/Data/BaseRepositoryTest.cs @@ -1,6 +1,7 @@ using Application.Orders; using Domain.Orders; using IntegrationTests.Common; +using Microsoft.EntityFrameworkCore; using WebApp; namespace IntegrationTests.Data; @@ -16,37 +17,15 @@ public BaseRepositoryTest(CustomWebApplicationFactory factory, BaseData } [Fact] - public async Task Given_A_Id_Then_Return_Order_With_Success() + public async Task GivenAIdThenReturnOrderDtoWithSuccess() { // Arrange var id = 1; // Act - var result = await _fixture!.repository!.GetByIdAsNoTrackingAsync( - id, - Guid.NewGuid(), - _fixture.cancellationToken, - includes: o => o.Items - ); - - // Assert - Assert.NotNull(result); - Assert.Equal(id, result!.Id); - Assert.NotNull(result.Items); - Assert.NotEmpty(result.Items); - } - - [Fact] - public async Task Given_A_Id_Then_Return_OrderDto_With_Success() - { - // Arrange - var id = 1; - - // Act - var result = await _fixture!.repository!.GetByIdAsNoTrackingAsync( - id, - Guid.NewGuid(), - selector: o => new OrderDto() + var result = await _fixture!.Repository!.GetQueryable(Guid.NewGuid()) + .Where(o => o.Id == id) + .Select(o => new OrderDto() { Id = o.Id, Total = o.Total, @@ -56,9 +35,7 @@ public async Task Given_A_Id_Then_Return_OrderDto_With_Success() Name = i.Name, Value = i.Value }).ToArray() - }, - _fixture.cancellationToken - ); + }).FirstOrDefaultAsync(_fixture.CancellationToken); // Assert Assert.NotNull(result); @@ -68,52 +45,65 @@ public async Task Given_A_Id_Then_Return_OrderDto_With_Success() } [Fact] - public async Task Given_A_Order_And_Notification_Should_Execute_In_Parallel_With_Success() + public async Task GivenAOrderAndNotificationShouldExecuteInParallelWithSuccess() { // Arrange var id = 1; // Act - var orderTask = _fixture!.repository!.GetByIdAsNoTrackingAsync( - id, - Guid.NewGuid(), - _fixture.cancellationToken - ); + var orderTask1 = _fixture!.Repository!.GetQueryable(Guid.NewGuid()) + .Where(o => o.Id == id) + .Select(o => new OrderDto() + { + Id = o.Id, + Total = o.Total, + Items = o.Items.Select(i => new ItemDto + { + Id = i.Id, + Name = i.Name, + Value = i.Value + }).ToArray() + }).FirstOrDefaultAsync(_fixture.CancellationToken); - var notificationTask = _fixture!.repository!.GetByIdAsNoTrackingAsync( - id, - Guid.NewGuid(), - _fixture.cancellationToken, - newContext: true - ); + var orderTask2 = _fixture!.Repository!.GetQueryable(Guid.NewGuid(), true) + .Where(n => n.Id == id) + .Select(o => new OrderDto() + { + Id = o.Id, + Total = o.Total, + Items = o.Items.Select(i => new ItemDto + { + Id = i.Id, + Name = i.Name, + Value = i.Value + }).ToArray() + }) + .FirstOrDefaultAsync(_fixture.CancellationToken); - await Task.WhenAll(orderTask, notificationTask); - var order = await orderTask; - var notification = await notificationTask; + await Task.WhenAll(orderTask1, orderTask2); + var order1 = await orderTask1; + var order2 = await orderTask2; // Assert - Assert.NotNull(order); - Assert.Equal(id, order!.Id); - - Assert.NotNull(notification); - Assert.Equal(id, notification!.Id); - Assert.NotNull(notification.NotificationType); - Assert.NotNull(notification.NotificationStatus); + Assert.NotNull(order1); + Assert.Equal(id, order1!.Id); + Assert.NotNull(order2); + Assert.Equal(id, order2!.Id); } [Fact] - public async Task Given_A_Valid_Request_Then_Return_All_Orders_Paginated_With_Success() + public async Task GivenAValidRequestThenReturnAllOrdersPaginatedWithSuccess() { // Arrange var pageNumber = 1; var pageSize = 5; // Act - var (result, totalRecords) = await _fixture!.repository!.GetAllPaginatedAsync( + var (result, totalRecords) = await _fixture!.Repository!.GetAllPaginatedAsync( Guid.NewGuid(), pageNumber, pageSize, - _fixture.cancellationToken + _fixture.CancellationToken ); // Assert @@ -123,14 +113,14 @@ public async Task Given_A_Valid_Request_Then_Return_All_Orders_Paginated_With_Su } [Fact] - public async Task Given_A_Valid_Request_Then_Return_All_OrdersDtos_Paginated_With_Success() + public async Task GivenAValidRequestThenReturnAllOrdersDtosPaginatedWithSuccess() { // Arrange var pageNumber = 1; var pageSize = 5; // Act - var (result, totalRecords) = await _fixture!.repository!.GetAllPaginatedAsync( + var (result, totalRecords) = await _fixture!.Repository!.GetAllPaginatedAsync( Guid.NewGuid(), pageNumber, pageSize, @@ -144,7 +134,7 @@ public async Task Given_A_Valid_Request_Then_Return_All_OrdersDtos_Paginated_Wit Value = i.Value }).ToArray() }, - _fixture.cancellationToken + _fixture.CancellationToken ); // Assert @@ -156,18 +146,18 @@ public async Task Given_A_Valid_Request_Then_Return_All_OrdersDtos_Paginated_Wit } [Fact] - public async Task Given_A_Valid_Request_Then_Return_No_Orders_Paginated() + public async Task GivenAValidRequestThenReturnNoOrdersPaginated() { // Arrange var pageNumber = 50; var pageSize = 5; // Act - var (result, totalRecords) = await _fixture!.repository!.GetAllPaginatedAsync( + var (result, totalRecords) = await _fixture!.Repository!.GetAllPaginatedAsync( Guid.NewGuid(), pageNumber, pageSize, - _fixture.cancellationToken + _fixture.CancellationToken ); // Assert @@ -177,7 +167,7 @@ public async Task Given_A_Valid_Request_Then_Return_No_Orders_Paginated() } [Fact] - public async Task Given_A_Valid_Request_Then_Return_Filtered_Orders_Paginated() + public async Task GivenAValidRequestThenReturnFilteredOrdersPaginated() { // Arrange var pageNumber = 1; @@ -188,11 +178,11 @@ public async Task Given_A_Valid_Request_Then_Return_Filtered_Orders_Paginated() }; // Act - var (result, totalRecords) = await _fixture!.repository!.GetAllPaginatedAsync( + var (result, totalRecords) = await _fixture!.Repository!.GetAllPaginatedAsync( Guid.NewGuid(), pageNumber, pageSize, - _fixture.cancellationToken, + _fixture.CancellationToken, searchByValues: searchByValues ); @@ -204,7 +194,7 @@ public async Task Given_A_Valid_Request_Then_Return_Filtered_Orders_Paginated() } [Fact] - public async Task Given_A_Valid_Request_Then_Return_No_Filtered_Orders_Paginated() + public async Task GivenAValidRequestThenReturnNoFilteredOrdersPaginated() { // Arrange var pageNumber = 1; @@ -214,11 +204,11 @@ public async Task Given_A_Valid_Request_Then_Return_No_Filtered_Orders_Paginated }; // Act - var (result, totalRecords) = await _fixture!.repository!.GetAllPaginatedAsync( + var (result, totalRecords) = await _fixture!.Repository!.GetAllPaginatedAsync( Guid.NewGuid(), pageNumber, pageSize, - _fixture.cancellationToken, + _fixture.CancellationToken, searchByValues: searchByValues ); @@ -231,7 +221,7 @@ public async Task Given_A_Valid_Request_Then_Return_No_Filtered_Orders_Paginated [Theory] [InlineData(true)] [InlineData(false)] - public async Task Given_A_Valid_Request_Then_Return_Sorted_Orders_Paginated(bool sortDescending) + public async Task GivenAValidRequestThenReturnSortedOrdersPaginated(bool sortDescending) { // Arrange var pageNumber = 1; @@ -239,11 +229,11 @@ public async Task Given_A_Valid_Request_Then_Return_Sorted_Orders_Paginated(bool var sortBy = "Description"; // Act - var (result, totalRecords) = await _fixture!.repository!.GetAllPaginatedAsync( + var (result, totalRecords) = await _fixture!.Repository!.GetAllPaginatedAsync( Guid.NewGuid(), pageNumber, pageSize, - _fixture.cancellationToken, + _fixture.CancellationToken, sortBy: sortBy, sortDescending: sortDescending ); diff --git a/templates/tests/IntegrationTests/GlobalUsings.cs b/templates/Full/tests/IntegrationTests/GlobalUsings.cs similarity index 100% rename from templates/tests/IntegrationTests/GlobalUsings.cs rename to templates/Full/tests/IntegrationTests/GlobalUsings.cs diff --git a/templates/tests/IntegrationTests/IntegrationTests.csproj b/templates/Full/tests/IntegrationTests/IntegrationTests.csproj similarity index 100% rename from templates/tests/IntegrationTests/IntegrationTests.csproj rename to templates/Full/tests/IntegrationTests/IntegrationTests.csproj diff --git a/templates/tests/IntegrationTests/WebApp/Grpc/Common/ApiGrpcHelper.cs b/templates/Full/tests/IntegrationTests/WebApp/Grpc/Common/ApiGrpcHelper.cs similarity index 59% rename from templates/tests/IntegrationTests/WebApp/Grpc/Common/ApiGrpcHelper.cs rename to templates/Full/tests/IntegrationTests/WebApp/Grpc/Common/ApiGrpcHelper.cs index b9c4f292..a97d6255 100644 --- a/templates/tests/IntegrationTests/WebApp/Grpc/Common/ApiGrpcHelper.cs +++ b/templates/Full/tests/IntegrationTests/WebApp/Grpc/Common/ApiGrpcHelper.cs @@ -3,10 +3,10 @@ namespace IntegrationTests.WebApp.Grpc.Common; public sealed class ApiGrpcHelper(HttpClient httpClient) { - public HttpClient httpClient = httpClient; + public HttpClient HttpClient { get; } = httpClient; - public GrpcChannel AsGrpcClientChannel() => GrpcChannel.ForAddress(httpClient.BaseAddress!, new GrpcChannelOptions + public GrpcChannel AsGrpcClientChannel() => GrpcChannel.ForAddress(HttpClient.BaseAddress!, new GrpcChannelOptions { - HttpClient = httpClient + HttpClient = HttpClient }); } diff --git a/templates/tests/IntegrationTests/WebApp/Grpc/Orders/GetOrderGrpcTest.cs b/templates/Full/tests/IntegrationTests/WebApp/Grpc/Orders/GetOrderGrpcTest.cs similarity index 73% rename from templates/tests/IntegrationTests/WebApp/Grpc/Orders/GetOrderGrpcTest.cs rename to templates/Full/tests/IntegrationTests/WebApp/Grpc/Orders/GetOrderGrpcTest.cs index f4ea7772..363e4920 100644 --- a/templates/tests/IntegrationTests/WebApp/Grpc/Orders/GetOrderGrpcTest.cs +++ b/templates/Full/tests/IntegrationTests/WebApp/Grpc/Orders/GetOrderGrpcTest.cs @@ -10,22 +10,22 @@ namespace IntegrationTests.WebApp.Grpc.Orders; [Collection("WebApplicationFactoryCollectionDefinition")] public class GetOrderGrpcTest : BaseFixture { - public CustomWebApplicationFactory customWebApplicationFactory; + public CustomWebApplicationFactory CustomWebApplicationFactory { get; } - public ApiGrpcHelper apiGrpcHelper; + public ApiGrpcHelper ApiGrpcHelper { get; } private readonly GrpcChannel _grpcChannel; private readonly OrderService.OrderServiceClient _service; public GetOrderGrpcTest(CustomWebApplicationFactory customWebApplicationFactory) { - this.customWebApplicationFactory = customWebApplicationFactory; - apiGrpcHelper = new(this.customWebApplicationFactory.CreateClient()); - _grpcChannel = apiGrpcHelper.AsGrpcClientChannel(); + CustomWebApplicationFactory = customWebApplicationFactory; + ApiGrpcHelper = new(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() + [Fact(DisplayName = nameof(GivenAValidRequestThenPass))] + public async Task GivenAValidRequestThenPass() { // Arrange GetOrderRequest request = new() @@ -48,8 +48,8 @@ public async Task Given_A_Valid_Request_Then_Pass() 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() + [Fact(DisplayName = nameof(GivenAInvalidRequestThenFails))] + public async Task GivenAInvalidRequestThenFails() { // Arrange GetOrderRequest request = new() diff --git a/templates/tests/IntegrationTests/WebApp/Http/Common/ApiHelper.cs b/templates/Full/tests/IntegrationTests/WebApp/Http/Common/ApiHelper.cs similarity index 60% rename from templates/tests/IntegrationTests/WebApp/Http/Common/ApiHelper.cs rename to templates/Full/tests/IntegrationTests/WebApp/Http/Common/ApiHelper.cs index d2962430..7bca09d0 100644 --- a/templates/tests/IntegrationTests/WebApp/Http/Common/ApiHelper.cs +++ b/templates/Full/tests/IntegrationTests/WebApp/Http/Common/ApiHelper.cs @@ -5,7 +5,7 @@ namespace IntegrationTests.WebApp.Http.Common; public sealed class ApiHelper(HttpClient httpClient) { - public HttpClient httpClient = httpClient; + public HttpClient HttpClient { get; } = httpClient; private static readonly JsonSerializerOptions _jsonSerializerOptions = new() { @@ -16,42 +16,37 @@ public void AddHeaders(Dictionary headers) { foreach (var header in headers) { - if (httpClient.DefaultRequestHeaders.Contains(header.Key)) + if (HttpClient.DefaultRequestHeaders.Contains(header.Key)) { - httpClient.DefaultRequestHeaders.Remove(header.Key); + HttpClient.DefaultRequestHeaders.Remove(header.Key); } - httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); + HttpClient.DefaultRequestHeaders.Add(header.Key, header.Value); } } public async Task GetAsync(string resourceUrl) => - await httpClient.GetAsync(resourceUrl); + await HttpClient.GetAsync(resourceUrl); public async Task PostAsync(string resourceUrl, dynamic dataClass) => - await httpClient.PostAsync(resourceUrl, SerializeRequest(dataClass)); + await HttpClient.PostAsync(resourceUrl, SerializeRequest(dataClass)); public async Task PutAsync(string resourceUrl, dynamic data) => - await httpClient.PutAsync(resourceUrl, SerializeRequest(data)); + await HttpClient.PutAsync(resourceUrl, SerializeRequest(data)); public async Task DeleteAsync(string resourceUrl) => - await httpClient.DeleteAsync(resourceUrl); + await HttpClient.DeleteAsync(resourceUrl); - public StringContent SerializeRequest(dynamic data) + public static StringContent SerializeRequest(dynamic data) { var json = JsonSerializer.Serialize(data); return new StringContent(json, Encoding.UTF8, "application/json"); } - public async Task DeSerializeResponse(HttpResponseMessage response) + public static async Task DeSerializeResponse(HttpResponseMessage response) { 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/tests/IntegrationTests/WebApp/Http/Common/BaseHttpFixture.cs b/templates/Full/tests/IntegrationTests/WebApp/Http/Common/BaseHttpFixture.cs similarity index 60% rename from templates/tests/IntegrationTests/WebApp/Http/Common/BaseHttpFixture.cs rename to templates/Full/tests/IntegrationTests/WebApp/Http/Common/BaseHttpFixture.cs index 64636bb8..ae8798d8 100644 --- a/templates/tests/IntegrationTests/WebApp/Http/Common/BaseHttpFixture.cs +++ b/templates/Full/tests/IntegrationTests/WebApp/Http/Common/BaseHttpFixture.cs @@ -6,9 +6,9 @@ namespace IntegrationTests.WebApp.Http.Common; public class BaseHttpFixture : BaseFixture { - public ApiHelper apiHelper; - public string resourceUrl = string.Empty; + public ApiHelper ApiHelper { get; set; } = null!; + public string ResourceUrl { get; set; } = string.Empty; public void SetApiHelper(CustomWebApplicationFactory customWebApplicationFactory) => - apiHelper = new(customWebApplicationFactory.CreateClient()); + ApiHelper = new(customWebApplicationFactory.CreateClient()); } \ No newline at end of file diff --git a/templates/tests/IntegrationTests/WebApp/Http/Orders/CreateOrderTest.cs b/templates/Full/tests/IntegrationTests/WebApp/Http/Orders/CreateOrderTest.cs similarity index 54% rename from templates/tests/IntegrationTests/WebApp/Http/Orders/CreateOrderTest.cs rename to templates/Full/tests/IntegrationTests/WebApp/Http/Orders/CreateOrderTest.cs index cf9af870..0d791355 100644 --- a/templates/tests/IntegrationTests/WebApp/Http/Orders/CreateOrderTest.cs +++ b/templates/Full/tests/IntegrationTests/WebApp/Http/Orders/CreateOrderTest.cs @@ -9,11 +9,23 @@ namespace IntegrationTests.WebApp.Http.Orders; public class CreateOrderTestFixture : BaseHttpFixture { - public CreateOrderRequest SetValidRequest() => autoFixture.Create(); + public CreateOrderRequest SetValidRequest() + { + var items = AutoFixture.Build() + .With(i => i.Value, AutoFixture.Create() + 1) // Ensure non-zero value + .CreateMany(2) + .ToArray(); + + return AutoFixture.Build() + .With(r => r.Items, items) + .With(r => r.TimezoneId, "UTC") // Use valid timezone ID + .Create(); + } - public CreateOrderRequest SetInvalidRequest() => autoFixture + public CreateOrderRequest SetInvalidRequest() => AutoFixture .Build() .With(r => r.Description, string.Empty) + .With(r => r.TimezoneId, "UTC") .Create(); } @@ -26,18 +38,18 @@ public CreateOrderTest(CustomWebApplicationFactory customWebApplication { _fixture = fixture; _fixture.SetApiHelper(customWebApplicationFactory); - _fixture.resourceUrl = "orders"; + _fixture.ResourceUrl = "orders"; } - [Fact(DisplayName = nameof(Given_A_Valid_Request_Then_Pass))] - public async Task Given_A_Valid_Request_Then_Pass() + [Fact(DisplayName = nameof(GivenAValidRequestThenPass))] + public async Task GivenAValidRequestThenPass() { // Arrange var request = _fixture.SetValidRequest(); // Act - var result = await _fixture.apiHelper.PostAsync(_fixture.resourceUrl, request); - var response = await _fixture.apiHelper.DeSerializeResponse>(result); + var result = await _fixture.ApiHelper.PostAsync(_fixture.ResourceUrl, request); + var response = await ApiHelper.DeSerializeResponse>(result); // Assert Assert.NotNull(result); @@ -46,15 +58,15 @@ public async Task Given_A_Valid_Request_Then_Pass() Assert.NotNull(response.Data); } - [Fact(DisplayName = nameof(Given_A_Invalid_Request_Then_Fails))] - public async Task Given_A_Invalid_Request_Then_Fails() + [Fact(DisplayName = nameof(GivenAInvalidRequestThenFails))] + public async Task GivenAInvalidRequestThenFails() { // Arrange var request = _fixture.SetInvalidRequest(); // Act - var result = await _fixture.apiHelper.PostAsync(_fixture.resourceUrl, request); - var response = await _fixture.apiHelper.DeSerializeResponse>(result); + var result = await _fixture.ApiHelper.PostAsync(_fixture.ResourceUrl, request); + var response = await ApiHelper.DeSerializeResponse>(result); // Assert Assert.NotNull(response); Assert.NotNull(result); diff --git a/templates/tests/IntegrationTests/WebApp/Http/Orders/GetAllOrdersTest.cs b/templates/Full/tests/IntegrationTests/WebApp/Http/Orders/GetAllOrdersTest.cs similarity index 55% rename from templates/tests/IntegrationTests/WebApp/Http/Orders/GetAllOrdersTest.cs rename to templates/Full/tests/IntegrationTests/WebApp/Http/Orders/GetAllOrdersTest.cs index 42d98d43..91eae446 100644 --- a/templates/tests/IntegrationTests/WebApp/Http/Orders/GetAllOrdersTest.cs +++ b/templates/Full/tests/IntegrationTests/WebApp/Http/Orders/GetAllOrdersTest.cs @@ -9,13 +9,13 @@ namespace IntegrationTests.WebApp.Http.Orders; public class GetAllOrdersTestFixture : BaseHttpFixture { - public BasePaginatedRequest SetValidRequest() => + public static BasePaginatedRequest SetValidRequest() => new(Guid.NewGuid(), 1, 10); - public BasePaginatedRequest SetInvalidPageRequest() => + public static BasePaginatedRequest SetInvalidPageRequest() => new(Guid.NewGuid(), 0, 10); - public BasePaginatedRequest SetInvalidPageSizeRequest() => + public static BasePaginatedRequest SetInvalidPageSizeRequest() => new(Guid.NewGuid(), 1, 0); } @@ -27,18 +27,18 @@ public GetAllOrdersTest(CustomWebApplicationFactory customWebApplicatio { _fixture = fixture; _fixture.SetApiHelper(customWebApplicationFactory); - _fixture.resourceUrl = "orders/paginated"; + _fixture.ResourceUrl = "orders/paginated"; } - [Fact(DisplayName = nameof(Given_A_Valid_Request_Then_Pass))] - public async Task Given_A_Valid_Request_Then_Pass() + [Fact(DisplayName = nameof(GivenAValidRequestThenPass))] + public async Task GivenAValidRequestThenPass() { // Arrange - var request = _fixture.SetValidRequest(); + var request = GetAllOrdersTestFixture.SetValidRequest(); // Act - var result = await _fixture.apiHelper.PostAsync(_fixture.resourceUrl, request); - var response = await _fixture.apiHelper.DeSerializeResponse>(result); + var result = await _fixture.ApiHelper.PostAsync(_fixture.ResourceUrl, request); + var response = await ApiHelper.DeSerializeResponse>(result); // Assert Assert.NotNull(result); @@ -49,15 +49,15 @@ public async Task Given_A_Valid_Request_Then_Pass() Assert.True(response.TotalRecords >= 0); } - [Fact(DisplayName = nameof(Given_An_Invalid_Page_Request_Then_Fails))] - public async Task Given_An_Invalid_Page_Request_Then_Fails() + [Fact(DisplayName = nameof(GivenAnInvalidPageRequestThenFails))] + public async Task GivenAnInvalidPageRequestThenFails() { // Arrange - var request = _fixture.SetInvalidPageRequest(); + var request = GetAllOrdersTestFixture.SetInvalidPageRequest(); // Act - var result = await _fixture.apiHelper.PostAsync(_fixture.resourceUrl, request); - var response = await _fixture.apiHelper.DeSerializeResponse>(result); + var result = await _fixture.ApiHelper.PostAsync(_fixture.ResourceUrl, request); + var response = await ApiHelper.DeSerializeResponse>(result); // Assert Assert.NotNull(result); @@ -66,15 +66,15 @@ public async Task Given_An_Invalid_Page_Request_Then_Fails() Assert.Contains("Page must be greater than 0", response.Message); } - [Fact(DisplayName = nameof(Given_An_Invalid_PageSize_Request_Then_Fails))] - public async Task Given_An_Invalid_PageSize_Request_Then_Fails() + [Fact(DisplayName = nameof(GivenAnInvalidPageSizeRequestThenFails))] + public async Task GivenAnInvalidPageSizeRequestThenFails() { // Arrange - var request = _fixture.SetInvalidPageSizeRequest(); + var request = GetAllOrdersTestFixture.SetInvalidPageSizeRequest(); // Act - var result = await _fixture.apiHelper.PostAsync(_fixture.resourceUrl, request); - var response = await _fixture.apiHelper.DeSerializeResponse>(result); + var result = await _fixture.ApiHelper.PostAsync(_fixture.ResourceUrl, request); + var response = await ApiHelper.DeSerializeResponse>(result); // Assert Assert.NotNull(result); @@ -83,8 +83,8 @@ public async Task Given_An_Invalid_PageSize_Request_Then_Fails() Assert.Contains("PageSize must be greater than 0", response.Message); } - [Fact(DisplayName = nameof(Given_An_Valid_Request_When_Pass_Search_By_Values_Filter_Then_Pass))] - public async Task Given_An_Valid_Request_When_Pass_Search_By_Values_Filter_Then_Pass() + [Fact(DisplayName = nameof(GivenAnValidRequestWhenPassSearchByValuesFilterThenPass))] + public async Task GivenAnValidRequestWhenPassSearchByValuesFilterThenPass() { // Arrange var request = new BasePaginatedRequest( @@ -93,8 +93,8 @@ public async Task Given_An_Valid_Request_When_Pass_Search_By_Values_Filter_Then_ ); // Act - var result = await _fixture.apiHelper.PostAsync(_fixture.resourceUrl, request); - var response = await _fixture.apiHelper.DeSerializeResponse>(result); + var result = await _fixture.ApiHelper.PostAsync(_fixture.ResourceUrl, request); + var response = await ApiHelper.DeSerializeResponse>(result); // Assert Assert.NotNull(result); diff --git a/templates/tests/IntegrationTests/WebApp/Http/Orders/GetOrderTest.cs b/templates/Full/tests/IntegrationTests/WebApp/Http/Orders/GetOrderTest.cs similarity index 60% rename from templates/tests/IntegrationTests/WebApp/Http/Orders/GetOrderTest.cs rename to templates/Full/tests/IntegrationTests/WebApp/Http/Orders/GetOrderTest.cs index cde446ae..58931b36 100644 --- a/templates/tests/IntegrationTests/WebApp/Http/Orders/GetOrderTest.cs +++ b/templates/Full/tests/IntegrationTests/WebApp/Http/Orders/GetOrderTest.cs @@ -1,4 +1,5 @@ -using System.Net; +using System.Globalization; +using System.Net; using Application.Common.Requests; using Application.Orders; using IntegrationTests.Common; @@ -15,23 +16,23 @@ public GetOrderTest(CustomWebApplicationFactory customWebApplicationFac { _fixture = fixture; _fixture.SetApiHelper(customWebApplicationFactory); - _fixture.resourceUrl = "orders/{0}"; + _fixture.ResourceUrl = "orders/{0}"; } - [Fact(DisplayName = nameof(Given_A_Valid_Request_Then_Pass))] - public async Task Given_A_Valid_Request_Then_Pass() + [Fact(DisplayName = nameof(GivenAValidRequestThenPass))] + public async Task GivenAValidRequestThenPass() { // Arrange var id = 1; - var url = string.Format(_fixture.resourceUrl, id); - _fixture.apiHelper.AddHeaders(new Dictionary + var url = string.Format(CultureInfo.InvariantCulture, _fixture.ResourceUrl, id); + _fixture.ApiHelper.AddHeaders(new Dictionary { { "CorrelationId", Guid.NewGuid().ToString() } }); // Act - var result = await _fixture.apiHelper.GetAsync(url); - var response = await _fixture.apiHelper.DeSerializeResponse>(result); + var result = await _fixture.ApiHelper.GetAsync(url); + var response = await ApiHelper.DeSerializeResponse>(result); var data = response?.Data; // Assert @@ -44,20 +45,20 @@ public async Task Given_A_Valid_Request_Then_Pass() Assert.NotEmpty(data.Items); } - [Fact(DisplayName = nameof(Given_A_Invalid_Request_Then_Fails))] - public async Task Given_A_Invalid_Request_Then_Fails() + [Fact(DisplayName = nameof(GivenAInvalidRequestThenFails))] + public async Task GivenAInvalidRequestThenFails() { // Arrange var id = 9999999; - var url = string.Format(_fixture.resourceUrl, id); - _fixture.apiHelper.AddHeaders(new Dictionary + var url = string.Format(CultureInfo.InvariantCulture, _fixture.ResourceUrl, id); + _fixture.ApiHelper.AddHeaders(new Dictionary { { "CorrelationId", Guid.NewGuid().ToString() } }); // Act - var result = await _fixture.apiHelper.GetAsync(url); - var response = await _fixture.apiHelper.DeSerializeResponse>(result); + var result = await _fixture.ApiHelper.GetAsync(url); + var response = await ApiHelper.DeSerializeResponse>(result); // Assert Assert.NotNull(result); diff --git a/templates/tests/IntegrationTests/WebApp/Messaging/Common/BaseMessagingFixture.cs b/templates/Full/tests/IntegrationTests/WebApp/Messaging/Common/BaseMessagingFixture.cs similarity index 62% rename from templates/tests/IntegrationTests/WebApp/Messaging/Common/BaseMessagingFixture.cs rename to templates/Full/tests/IntegrationTests/WebApp/Messaging/Common/BaseMessagingFixture.cs index 98e0a23c..b426e1ce 100644 --- a/templates/tests/IntegrationTests/WebApp/Messaging/Common/BaseMessagingFixture.cs +++ b/templates/Full/tests/IntegrationTests/WebApp/Messaging/Common/BaseMessagingFixture.cs @@ -10,20 +10,20 @@ namespace IntegrationTests.WebApp.Messaging.Common; public class BaseMessagingFixture : BaseFixture { - public IProduceService produceService; - public IBaseRepository repository; + public IProduceService ProduceService { get; set; } = null!; + public IBaseRepository Repository { get; set; } = null!; public void SetServices(AsyncServiceScope scope) { - produceService = scope.ServiceProvider.GetRequiredService(); - repository = scope.ServiceProvider.GetRequiredService(); + ProduceService = scope.ServiceProvider.GetRequiredService(); + Repository = scope.ServiceProvider.GetRequiredService(); } public void SetServices(CustomWebApplicationFactory factory) { var scope = factory.Services.CreateAsyncScope(); - produceService = scope.ServiceProvider.GetRequiredService(); - repository = scope.ServiceProvider.GetRequiredService(); + ProduceService = scope.ServiceProvider.GetRequiredService(); + Repository = scope.ServiceProvider.GetRequiredService(); } public async Task HandleProducerAsync( @@ -32,8 +32,8 @@ public async Task HandleProducerAsync( int delay = 1500 ) where TMessage : BaseMessage { - await produceService.HandleAsync(message, cancellationToken, queueName); + await ProduceService.HandleAsync(message, CancellationToken, queueName); - await Task.Delay(delay, cancellationToken); + await Task.Delay(delay, CancellationToken); } } diff --git a/templates/tests/IntegrationTests/WebApp/Messaging/Notifications/CreateNotificationTest.cs b/templates/Full/tests/IntegrationTests/WebApp/Messaging/Notifications/CreateNotificationTest.cs similarity index 55% rename from templates/tests/IntegrationTests/WebApp/Messaging/Notifications/CreateNotificationTest.cs rename to templates/Full/tests/IntegrationTests/WebApp/Messaging/Notifications/CreateNotificationTest.cs index e50c394b..9894aed0 100644 --- a/templates/tests/IntegrationTests/WebApp/Messaging/Notifications/CreateNotificationTest.cs +++ b/templates/Full/tests/IntegrationTests/WebApp/Messaging/Notifications/CreateNotificationTest.cs @@ -1,10 +1,11 @@ -using Application.Common.Constants; using Application.Common.Messages; using Domain.Notifications; using IntegrationTests.Common; using IntegrationTests.WebApp.Messaging.Common; using Microsoft.Extensions.DependencyInjection; using WebApp; +using Microsoft.EntityFrameworkCore; +using Domain.Common.Enums; namespace IntegrationTests.WebApp.Messaging.Notifications; @@ -16,7 +17,9 @@ public class CreateNotificationTestFixture : BaseMessagingFixture SetServices(scope); } - public CreateNotificationMessage SetValidMessage() => autoFixture.Build().Create(); + public CreateNotificationMessage SetValidMessage() => AutoFixture.Build() + .With(m => m.NotificationType, NotificationType.OrderCreated) + .Create(); } [Collection("WebApplicationFactoryCollectionDefinition")] @@ -30,41 +33,37 @@ public CreateNotificationTest(CustomWebApplicationFactory factory, Crea _fixture.SetServices(factory); } - [Fact(DisplayName = nameof(Given_A_Valid_Message_Then_Pass))] - public async Task Given_A_Valid_Message_Then_Pass() + [Fact(DisplayName = nameof(GivenAValidMessageThenPass))] + public async Task GivenAValidMessageThenPass() { // Arrange var message = _fixture.SetValidMessage(); // Act - await _fixture.HandleProducerAsync(message, NotificationType.OrderCreated); + await _fixture.HandleProducerAsync(message, NotificationType.OrderCreated.ToString()); - var notification = await _fixture.repository.FirstOrDefaultAsNoTrackingAsync( - Guid.NewGuid(), - n => n.NotificationType == message.NotificationType && n.NotificationStatus == message.NotificationStatus, - _fixture.cancellationToken - ); + var notification = await _fixture.Repository.GetQueryable(Guid.NewGuid()) + .Where(n => n.NotificationType == message.NotificationType && n.NotificationStatus == message.NotificationStatus) + .FirstOrDefaultAsync(_fixture.CancellationToken); // Assert Assert.NotNull(notification); Assert.Equal(message.NotificationType, notification.NotificationType); } - [Fact(DisplayName = nameof(Given_A_Duplicate_Message_Then_Should_Not_Create_Duplicated_Message))] - public async Task Given_A_Duplicate_Message_Then_Should_Not_Create_Duplicated_Message() + [Fact(DisplayName = nameof(GivenADuplicateMessageThenShouldNotCreateDuplicatedMessage))] + public async Task GivenADuplicateMessageThenShouldNotCreateDuplicatedMessage() { // Arrange var message = _fixture.SetValidMessage(); // Act - await _fixture.HandleProducerAsync(message, NotificationType.OrderCreated); - await _fixture.HandleProducerAsync(message, NotificationType.OrderCreated); + await _fixture.HandleProducerAsync(message, NotificationType.OrderCreated.ToString()); + await _fixture.HandleProducerAsync(message, NotificationType.OrderCreated.ToString()); - var notifications = await _fixture.repository.GetByWhereAsNoTrackingAsync( - Guid.NewGuid(), - n => n.NotificationType == message.NotificationType && n.NotificationStatus == message.NotificationStatus, - cancellationToken: _fixture.cancellationToken - ); + var notifications = await _fixture.Repository.GetQueryable(Guid.NewGuid()) + .Where(n => n.NotificationType == message.NotificationType && n.NotificationStatus == message.NotificationStatus) + .ToListAsync(_fixture.CancellationToken); // Assert Assert.Single(notifications); diff --git a/templates/Full/tests/IntegrationTests/packages.lock.json b/templates/Full/tests/IntegrationTests/packages.lock.json new file mode 100644 index 00000000..3a5f57ad --- /dev/null +++ b/templates/Full/tests/IntegrationTests/packages.lock.json @@ -0,0 +1,928 @@ +{ + "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" + } + }, + "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", + "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.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.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": "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.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.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "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" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "Npgsql": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "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==" + }, + "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==" + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "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]" + } + }, + "application": { + "type": "Project", + "dependencies": { + "Domain": "[1.0.0, )", + "FluentValidation": "[12.1.1, )", + "FluentValidation.DependencyInjectionExtensions": "[12.1.1, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.Extensions.Logging": "[10.0.5, )" + } + }, + "commontests": { + "type": "Project", + "dependencies": { + "AutoFixture": "[4.18.1, )", + "Newtonsoft.Json": "[13.0.4, )", + "xunit.extensibility.core": "[2.9.3, )" + } + }, + "domain": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "[10.0.5, )" + } + }, + "infrastructure": { + "type": "Project", + "dependencies": { + "Application": "[1.0.0, )", + "Microsoft.Extensions.Caching.Hybrid": "[10.4.0, )", + "Microsoft.Extensions.Caching.StackExchangeRedis": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", + "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.EntityFrameworkCore": "[1.15.0-beta.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, )", + "RabbitMQ.Client": "[7.2.1, )", + "RabbitMQ.Client.OpenTelemetry": "[1.0.0-rc.2, )" + } + }, + "webapp": { + "type": "Project", + "dependencies": { + "AspNetCore.HealthChecks.Npgsql": "[9.0.0, )", + "AspNetCore.HealthChecks.RabbitMQ": "[9.0.0, )", + "AspNetCore.HealthChecks.Redis": "[9.0.0, )", + "AspNetCore.HealthChecks.UI.Client": "[9.0.0, )", + "Grpc.AspNetCore": "[2.76.0, )", + "Infrastructure": "[1.0.0, )" + } + }, + "AspNetCore.HealthChecks.NpgSql": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "npc58/AD5zuVxERdhCl2Kb7WnL37mwX42SJcXIwvmEig0/dugOLg3SIwtfvvh3TnvTwR/sk5LYNkkPaBdks61A==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11", + "Npgsql": "8.0.3" + } + }, + "AspNetCore.HealthChecks.Rabbitmq": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "7WSQ7EwioA5niakzzLtGVcZMEOh+42fSwrI24vnNsT7gZuVGOViNekyz38G6wBPYKcpL/lUkMdg3ZaCiZTi/Dw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11", + "RabbitMQ.Client": "7.0.0" + } + }, + "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" + } + }, + "AutoFixture": { + "type": "CentralTransitive", + "requested": "[4.18.1, )", + "resolved": "4.18.1", + "contentHash": "BmWZDY4fkrYOyd5/CTBOeXbzsNwV8kI4kDi/Ty1Y5F+WDHBVKxzfWlBE4RSicvZ+EOi2XDaN5uwdrHsItLW6Kw==", + "dependencies": { + "Fare": "[2.1.1, 3.0.0)" + } + }, + "FluentValidation": { + "type": "CentralTransitive", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" + }, + "FluentValidation.DependencyInjectionExtensions": { + "type": "CentralTransitive", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "D0VXh4dtjjX2aQizuaa0g6R8X3U1JaVqJPfGCvLwZX9t/O2h7tkpbitbadQMfwcgSPdDbI2vDxuwRMv/Uf9dHA==", + "dependencies": { + "FluentValidation": "12.1.1", + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.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.EntityFrameworkCore": { + "type": "CentralTransitive", + "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.EntityFrameworkCore.Relational": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "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.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==" + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" + } + }, + "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.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[1.15.0-beta.1, )", + "resolved": "1.15.0-beta.1", + "contentHash": "N01GzP+r8lpSBiqjRX0/WjSp17r+zk6dKvGKthiASyFzF44lrJo8cA3ihXnw3p4Rnqg1mVjOYy19R6iJ84NTpg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "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" + } + }, + "RabbitMQ.Client": { + "type": "CentralTransitive", + "requested": "[7.2.1, )", + "resolved": "7.2.1", + "contentHash": "YKXEfg9fVQiTKgZlvIhAfPSFaamEgi8DsQmisCH0IAsU4FYLrtoguDrDj6JtJVGUt40QPnBLRH6fTQcAC4qsOg==", + "dependencies": { + "System.Threading.RateLimiting": "8.0.0" + } + }, + "RabbitMQ.Client.OpenTelemetry": { + "type": "CentralTransitive", + "requested": "[1.0.0-rc.2, )", + "resolved": "1.0.0-rc.2", + "contentHash": "fVjEZ8DsLDw3EEp/Q5XGFhk7Rvluh32n+38kyBcCdZu/F7cOuj6ETo+pYMhXHtcKGk856sEEOaEWR+alsETv9w==", + "dependencies": { + "OpenTelemetry.Api": "1.9.0", + "RabbitMQ.Client": "7.2.0" + } + }, + "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/Full/tests/LoadTests/protos/order.proto b/templates/Full/tests/LoadTests/protos/order.proto new file mode 100644 index 00000000..53b2838d --- /dev/null +++ b/templates/Full/tests/LoadTests/protos/order.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package order; + +service OrderService { + rpc Get (GetOrderRequest) returns (OrderReply); +} + +message GetOrderRequest { + int32 id = 1; + string correlationId = 2; +} + +message OrderDto { + int32 id = 1; + string description = 2; + double total = 3; +} + +message OrderReply { + bool success = 1; + string message = 2; + OrderDto data = 3; +} \ No newline at end of file diff --git a/templates/Full/tests/LoadTests/scriptGrpc.js b/templates/Full/tests/LoadTests/scriptGrpc.js new file mode 100644 index 00000000..1bef07a9 --- /dev/null +++ b/templates/Full/tests/LoadTests/scriptGrpc.js @@ -0,0 +1,52 @@ +import grpc from 'k6/net/grpc'; +import { check, sleep } from 'k6'; +import { Counter, Trend, Rate } from 'k6/metrics'; + +export const options = { + scenarios: { + get_order: { + exec: 'getOrder', + executor: 'constant-vus', + vus: __ENV.VUS ? parseInt(__ENV.VUS) : 10, + duration: __ENV.DURATION ? __ENV.DURATION : '60s', + gracefulStop: __ENV.GRACEFUL_STOP ? __ENV.GRACEFUL_STOP : '10s' + } + }, + thresholds: { + grpc_req_duration: ['p(50) < 50', 'p(95) < 100', 'p(99.9) < 500'], + get_order_response_time: ['p(95) < 100'], + get_order_success_rate: ['rate>0.95'], + get_order_requests_total: ['count>500'] + } +}; + +const webappUrl = __ENV.WEBAPP_GRPC_URL || 'localhost:7175'; + +const client = new grpc.Client(); +client.load([], './protos/order.proto'); + +const getOrderRequestsCounter = new Counter('get_order_requests_total'); +const getOrderResponseTime = new Trend('get_order_response_time'); +const getOrderSuccessRate = new Rate('get_order_success_rate'); +export function getOrder() { + getOrderRequestsCounter.add(1); + + client.connect(webappUrl, { plaintext: false }); + + const startTime = Date.now(); + const request = { id: 1, correlationId: crypto.randomUUID() }; + const response = client.invoke('order.OrderService/Get', request); + const duration = Date.now() - startTime; + + getOrderResponseTime.add(duration); + + const checkResults = check(response, { + 'status is OK': (r) => r && r.status === grpc.StatusOK, + 'success': (r) => r.message && r.message.success === true, + }); + + getOrderSuccessRate.add(checkResults); + + client.close(); + sleep(1); +} diff --git a/templates/Full/tests/LoadTests/scriptHttp.js b/templates/Full/tests/LoadTests/scriptHttp.js new file mode 100644 index 00000000..c870acb4 --- /dev/null +++ b/templates/Full/tests/LoadTests/scriptHttp.js @@ -0,0 +1,53 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { Counter, Trend, Rate } from 'k6/metrics'; + +export const options = { + scenarios: { + get_order: { + exec: 'getOrder', + executor: 'constant-vus', + vus: __ENV.VUS ? parseInt(__ENV.VUS) : 10, + duration: __ENV.DURATION ? __ENV.DURATION : '60s', + gracefulStop: __ENV.GRACEFUL_STOP ? __ENV.GRACEFUL_STOP : '10s' + } + }, + thresholds: { + http_req_duration: ['p(50) < 100', 'p(95) < 500', 'p(99.9) < 1000'], + http_req_failed: ['rate<0.1'], + get_order_response_time: ['p(95) < 500'], + get_order_success_rate: ['rate>0.95'], + get_order_requests_total: ['count>500'] + }, +}; + +const webappUrl = __ENV.WEBAPP_URL || 'https://localhost:7175'; + +const headers = { + headers: { + 'correlationId': crypto.randomUUID(), + 'Accept': 'application/json', + 'Accept-Encoding': 'gzip, deflate', + 'CacheEnabled': 'false' + } +}; + +const getOrderRequestsCounter = new Counter('get_order_requests_total'); +const getOrderResponseTime = new Trend('get_order_response_time'); +const getOrderSuccessRate = new Rate('get_order_success_rate'); +export function getOrder() { + getOrderRequestsCounter.add(1); + + const res = http.get(`${webappUrl}/orders/1`, headers); + + getOrderResponseTime.add(res.timings.duration); + + const checkResults = check(res, { + 'status is 200': (r) => r.status === 200, + 'content type is JSON': (r) => r.headers['Content-Type'] === 'application/json; charset=utf-8', + }); + + getOrderSuccessRate.add(checkResults); + + sleep(1); +} diff --git a/templates/tests/UnitTests/Application/Common/BaseApplicationFixture.cs b/templates/Full/tests/UnitTests/Application/Common/BaseApplicationFixture.cs similarity index 51% rename from templates/tests/UnitTests/Application/Common/BaseApplicationFixture.cs rename to templates/Full/tests/UnitTests/Application/Common/BaseApplicationFixture.cs index c54b40e9..11ddb06f 100644 --- a/templates/tests/UnitTests/Application/Common/BaseApplicationFixture.cs +++ b/templates/Full/tests/UnitTests/Application/Common/BaseApplicationFixture.cs @@ -13,54 +13,56 @@ public class BaseApplicationFixture : BaseFixture where TRequest : class where TUseCase : class { - public Mock mockServiceProvider = new(); - public Mock mockLogger = new(); - public Mock mockLoggerFactory = new(); - public Mock mockProduceService = new(); - public Mock mockRepository = new(); - public Mock> mockValidator = new(); - public Mock mockCache = new(); - public TUseCase useCase = default!; + public Mock MockServiceProvider { get; } = new(); + public Mock MockLogger { get; } = new(); + public Mock MockLoggerFactory { get; } = new(); + public Mock MockProduceService { get; } = new(); + public Mock MockRepository { get; } = new(); + public Mock> MockValidator { get; } = new(); + public Mock MockCache { get; } = new(); + public TUseCase UseCase { get; set; } = default!; public BaseApplicationFixture() { + MockLogger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + MockServiceProviderServices(); } public void MockServiceProviderServices() { - mockServiceProvider + MockServiceProvider .Setup(r => r.GetService(typeof(ILoggerFactory))) - .Returns(mockLoggerFactory.Object); + .Returns(MockLoggerFactory.Object); - mockLoggerFactory + MockLoggerFactory .Setup(l => l.CreateLogger(It.IsAny())) - .Returns(mockLogger.Object); + .Returns(MockLogger.Object); - mockServiceProvider - .Setup(r => r.GetService(typeof(IValidator))) - .Returns(mockValidator.Object); + MockServiceProvider + .Setup(r => r.GetService(typeof(IValidator))) + .Returns(MockValidator.Object); - mockServiceProvider + MockServiceProvider .Setup(r => r.GetService(typeof(IHybridCacheService))) - .Returns(mockCache.Object); + .Returns(MockCache.Object); - mockServiceProvider + MockServiceProvider .Setup(r => r.GetService(typeof(IProduceService))) - .Returns(mockProduceService.Object); + .Returns(MockProduceService.Object); - mockServiceProvider + MockServiceProvider .Setup(r => r.GetService(typeof(IBaseRepository))) - .Returns(mockRepository.Object); + .Returns(MockRepository.Object); } public void ClearInvocations() { - mockLogger.Reset(); - mockValidator.Reset(); - mockCache.Reset(); - mockProduceService.Reset(); - mockRepository.Reset(); + MockLogger.Invocations.Clear(); + MockValidator.Reset(); + MockCache.Reset(); + MockProduceService.Reset(); + MockRepository.Reset(); } public BasePaginatedRequest SetValidBasePaginatedRequest() => new(Guid.NewGuid(), 1, 10); @@ -68,8 +70,8 @@ public void ClearInvocations() public void SetSuccessfulValidator(TRequest request) { var validationResult = new ValidationResult(); - mockValidator - .Setup(v => v.ValidateAsync(request, cancellationToken)) + MockValidator + .Setup(v => v.ValidateAsync(request, CancellationToken)) .ReturnsAsync(validationResult); } @@ -79,45 +81,29 @@ public void SetFailedValidator(TRequest request) { Errors = [new("Description", "Description is required")] }; - mockValidator - .Setup(v => v.ValidateAsync(request, cancellationToken)) + MockValidator + .Setup(v => v.ValidateAsync(request, CancellationToken)) .ReturnsAsync(validationResult); } - public void SetValidGetOrCreateAsync(TResult result) => mockCache + public void SetValidGetOrCreateAsync(TResult result) => MockCache .Setup(c => c.GetOrCreateAsync( + It.IsAny(), It.IsAny(), It.IsAny>>(), It.IsAny() )).ReturnsAsync(result); - public void SetInvalidGetOrCreateAsync() => mockCache.Setup(c => c.GetOrCreateAsync( + public void SetInvalidGetOrCreateAsync() => MockCache.Setup(c => c.GetOrCreateAsync( + It.IsAny(), It.IsAny(), It.IsAny>>(), It.IsAny() )); - public void VerifyStartUseCaseLog(int times = 1) => VerifyLogInformation("Start to execute use case", times); - public void VerifyFinishUseCaseLog(int times = 1) => VerifyLogInformation("Finished executing use case", times); - public void VerifyFinishUseCaseWithCacheLog(int times = 1) => VerifyLogInformation("Finished executing use case with cache key", times); - - public void VerifyLogInformation(string message, int times = 1) => mockLogger.VerifyLog( - l => l.LogInformation($"*{message}*"), - Times.Exactly(times) - ); - - public void VerifyLogWarning(string message, int times = 1) => mockLogger.VerifyLog( - l => l.LogWarning($"*{message}*"), - Times.Exactly(times) - ); - - public void VerifyLogError(string message, int times = 1) => mockLogger.VerifyLog( - l => l.LogError($"*{message}*"), - Times.Exactly(times) - ); - - public void VerifyCache(int times) => mockCache.Verify( + public void VerifyCache(int times) => MockCache.Verify( c => c.GetOrCreateAsync( + It.IsAny(), It.IsAny(), It.IsAny>>(), It.IsAny() @@ -125,7 +111,7 @@ public void VerifyCache(int times) => mockCache.Verify( Times.Exactly(times) ); - public void VerifyProduce(int times = 1) where TMessage : BaseMessage => mockProduceService.Verify( + public void VerifyProduce(int times = 1) where TMessage : BaseMessage => MockProduceService.Verify( p => p.HandleAsync( It.IsAny(), It.IsAny(), diff --git a/templates/Full/tests/UnitTests/Application/Common/LogMockExtensions.cs b/templates/Full/tests/UnitTests/Application/Common/LogMockExtensions.cs new file mode 100644 index 00000000..c054d1e0 --- /dev/null +++ b/templates/Full/tests/UnitTests/Application/Common/LogMockExtensions.cs @@ -0,0 +1,62 @@ +using Microsoft.Extensions.Logging; + +namespace UnitTests.Application.Common; + +internal static class LogMockExtensions +{ + public static void VerifyDebug(this Mock mockLogger, string expectedMessage, int times = 1) => mockLogger.Verify( + logger => logger.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains(expectedMessage)), + It.IsAny(), + It.Is>((v, t) => true) + ), + Times.Exactly(times) + ); + + public static void VerifyInformation(this Mock mockLogger, string expectedMessage, int times = 1) => mockLogger.Verify( + logger => logger.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains(expectedMessage)), + It.IsAny(), + It.Is>((v, t) => true) + ), + Times.Exactly(times) + ); + + public static void VerifyError(this Mock mockLogger, string expectedMessage, int times = 1) => mockLogger.Verify( + logger => logger.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains(expectedMessage)), + It.IsAny(), + It.Is>((v, t) => true) + ), + Times.Exactly(times) + ); + + public static void VerifyWarning(this Mock mockLogger, string expectedMessage, int times = 1) => mockLogger.Verify( + logger => logger.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains(expectedMessage)), + It.IsAny(), + It.Is>((v, t) => true) + ), + Times.Exactly(times) + ); + + public static void VerifyStartOperation(this Mock mockLogger, int times = 1) => + VerifyInformation(mockLogger, $"Starting operation", times); + + public static void VerifyFinishOperation(this Mock mockLogger, int times = 1) => + VerifyInformation(mockLogger, $"Finished operation", times); + + public static void VerifyOperationFailed(this Mock mockLogger, int times = 1) => + VerifyWarning(mockLogger, "Failed operation", times); + + public static void VerifyNotFound(this Mock mockLogger, int times = 1) => + VerifyWarning(mockLogger, $"not found.", times); +} \ No newline at end of file diff --git a/templates/tests/UnitTests/Application/Common/RepositoryMockExtensions.cs b/templates/Full/tests/UnitTests/Application/Common/RepositoryMockExtensions.cs similarity index 62% rename from templates/tests/UnitTests/Application/Common/RepositoryMockExtensions.cs rename to templates/Full/tests/UnitTests/Application/Common/RepositoryMockExtensions.cs index 6473224a..e61b1216 100644 --- a/templates/tests/UnitTests/Application/Common/RepositoryMockExtensions.cs +++ b/templates/Full/tests/UnitTests/Application/Common/RepositoryMockExtensions.cs @@ -1,10 +1,11 @@ using System.Linq.Expressions; using Application.Common.Repositories; using Domain.Common; +using MockQueryable; namespace UnitTests.Application.Common; -public static class RepositoryMockExtensions +internal static class RepositoryMockExtensions { public static void SetSuccessfulAddAsync(this Mock mockRepository) where TEntity : DomainEntity => mockRepository .Setup(d => d.AddAsync(It.IsAny(), It.IsAny(), It.IsAny())) @@ -14,39 +15,11 @@ public static void SetFailedAddAsync(this Mock mockRep .Setup(d => d.AddAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(0); - public static void SetupGetByIdAsNoTrackingAsync(this Mock mockRepository, TEntity entity) where TEntity : DomainEntity => mockRepository - .Setup(r => r.GetByIdAsNoTrackingAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>[]>() - )).ReturnsAsync(entity); - - public static void SetupGetByIdAsNoTrackingAsync(this Mock mockRepository, TResult result) where TEntity : DomainEntity => mockRepository - .Setup(r => r.GetByIdAsNoTrackingAsync( - It.IsAny(), - It.IsAny(), - It.IsAny>>(), - It.IsAny() - )).ReturnsAsync(result); - - public static void SetupGetByIdAsNoTrackingAsyncNotFound(this Mock mockRepository) where TEntity : DomainEntity => mockRepository - .Setup(r => r.GetByIdAsNoTrackingAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>[]>() - )).ReturnsAsync((TEntity) null!); + public static void SetupQueryable(this Mock mockRepository, ICollection entities) where TEntity : DomainEntity => mockRepository + .Setup(r => r.GetQueryable(It.IsAny(), It.IsAny(), It.IsAny())).Returns(entities.BuildMock()); - public static void SetupGetByIdAsNoTrackingAsyncNotFound(this Mock mockRepository) where TEntity : DomainEntity => mockRepository - .Setup(r => r.GetByIdAsNoTrackingAsync( - It.IsAny(), - It.IsAny(), - It.IsAny>>(), - It.IsAny() - )); + public static void SetupQueryable(this Mock mockRepository, Guid correlationId, bool? newContext, ICollection entities) where TEntity : DomainEntity => mockRepository + .Setup(r => r.GetQueryable(correlationId, newContext, It.IsAny())).Returns(entities.BuildMock()); public static void VerifyAddAsync(this Mock mockRepository, int times) where TEntity : DomainEntity => mockRepository.Verify( d => d.AddAsync(It.IsAny(), It.IsAny(), It.IsAny()), @@ -94,7 +67,7 @@ public static void SetInvalidGetAllPaginatedAsync(this Mock>>() )).ReturnsAsync(([], 0)); - public static void VerifyGetAllPaginatedNoIncludes(this Mock mockRepository, int times) where TEntity : DomainEntity where TResult : class => mockRepository + public static void VerifyGetAllPaginatedNoIncludes(this Mock mockRepository, int times = 1) where TEntity : DomainEntity where TResult : class => mockRepository .Verify(r => r.GetAllPaginatedAsync( It.IsAny(), It.IsAny(), @@ -107,12 +80,7 @@ public static void VerifyGetAllPaginatedNoIncludes(this Mock>>() ), Times.Exactly(times)); - public static void VerifyGetByIdAsync(this Mock mockRepository, int times) where TEntity : DomainEntity => mockRepository - .Verify(r => r.GetByIdAsNoTrackingAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>[]>() + public static void VerifyQueryable(this Mock mockRepository, int times = 1) where TEntity : DomainEntity => mockRepository + .Verify(r => r.GetQueryable(It.IsAny(), It.IsAny(), It.IsAny() ), Times.Exactly(times)); } diff --git a/templates/tests/UnitTests/Application/Notifications/CreateNotificationUseCaseTests.cs b/templates/Full/tests/UnitTests/Application/Notifications/CreateNotificationUseCaseTests.cs similarity index 54% rename from templates/tests/UnitTests/Application/Notifications/CreateNotificationUseCaseTests.cs rename to templates/Full/tests/UnitTests/Application/Notifications/CreateNotificationUseCaseTests.cs index 80b6c69f..d6bb7bf0 100644 --- a/templates/tests/UnitTests/Application/Notifications/CreateNotificationUseCaseTests.cs +++ b/templates/Full/tests/UnitTests/Application/Notifications/CreateNotificationUseCaseTests.cs @@ -1,18 +1,18 @@ using Application.Notifications; +using Domain.Common.Enums; using Domain.Notifications; using FluentValidation; using FluentValidation.TestHelper; -using Microsoft.Extensions.Logging; using UnitTests.Application.Common; namespace UnitTests.Application.Notifications; public sealed class CreateNotificationRequestValidationFixture { - public IValidator validator = new CreateNotificationRequestValidator(); + public IValidator Validator { get; } = new CreateNotificationRequestValidator(); - public CreateNotificationRequest GetValidRequest() => - new(Guid.NewGuid(), "TestNotification", "Success", "System", new { Test = "Message" }); + public static CreateNotificationRequest GetValidRequest() => + new(Guid.NewGuid(), NotificationType.OrderCreated, "Success", "System", new { Test = "Message" }); } public sealed class CreateNotificationRequestValidationTests(CreateNotificationRequestValidationFixture fixture) : IClassFixture @@ -23,10 +23,10 @@ public sealed class CreateNotificationRequestValidationTests(CreateNotificationR public async Task GivenAValidRequestThenPass() { // Arrange - var request = _fixture.GetValidRequest(); + var request = CreateNotificationRequestValidationFixture.GetValidRequest(); // Act - var result = await _fixture.validator.TestValidateAsync(request); + var result = await _fixture.Validator.TestValidateAsync(request); // Assert result.ShouldNotHaveAnyValidationErrors(); @@ -36,14 +36,14 @@ public async Task GivenAValidRequestThenPass() public async Task GivenAnInvalidRequestThenFails() { // Arrange - var request = _fixture.GetValidRequest() with + var request = CreateNotificationRequestValidationFixture.GetValidRequest() with { CorrelationId = Guid.Empty, - NotificationType = string.Empty + NotificationType = (NotificationType)(-1) }; // Act - var result = await _fixture.validator.TestValidateAsync(request); + var result = await _fixture.Validator.TestValidateAsync(request); // Assert result.ShouldHaveValidationErrorFor("CorrelationId"); @@ -55,14 +55,11 @@ public sealed class CreateNotificationUseCaseFixture : BaseApplicationFixture - new(Guid.NewGuid(), "TestNotification", "Success", "System", new { Test = "Message" }); - - public void VerifyFailedToCreateNotificationLog(int times = 1) => - mockLogger.VerifyLog(l => l.LogWarning("*Failed to create notification.*"), Times.Exactly(times)); + public static CreateNotificationRequest SetValidRequest() => + new(Guid.NewGuid(), NotificationType.OrderCreated, "Success", "System", new { Test = "Message" }); } public sealed class CreateNotificationUseCaseTests : IClassFixture @@ -79,52 +76,52 @@ public CreateNotificationUseCaseTests(CreateNotificationUseCaseFixture fixture) public async Task GivenAValidRequestThenPass() { // Arrange - var request = _fixture.SetValidRequest(); + var request = CreateNotificationUseCaseFixture.SetValidRequest(); _fixture.SetSuccessfulValidator(request); - _fixture.mockRepository.SetSuccessfulAddAsync(); + _fixture.MockRepository.SetSuccessfulAddAsync(); // Act - await _fixture.useCase.HandleAsync(request, _fixture.cancellationToken); + await _fixture.UseCase.HandleAsync(request, _fixture.CancellationToken); // Assert - _fixture.VerifyStartUseCaseLog(); - _fixture.VerifyFinishUseCaseLog(); - _fixture.VerifyFailedToCreateNotificationLog(0); - _fixture.mockRepository.VerifyAddAsync(1); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockLogger.VerifyFinishOperation(); + _fixture.MockLogger.VerifyOperationFailed(0); + _fixture.MockRepository.VerifyAddAsync(1); } [Fact(DisplayName = nameof(GivenAnInvalidRequestThenFails))] public async Task GivenAnInvalidRequestThenFails() { // Arrange - var request = _fixture.SetValidRequest(); + var request = CreateNotificationUseCaseFixture.SetValidRequest(); _fixture.SetFailedValidator(request); // Act - await _fixture.useCase.HandleAsync(request, _fixture.cancellationToken); + await _fixture.UseCase.HandleAsync(request, _fixture.CancellationToken); // Assert - _fixture.VerifyStartUseCaseLog(); - _fixture.VerifyFinishUseCaseLog(0); - _fixture.VerifyFailedToCreateNotificationLog(0); - _fixture.mockRepository.VerifyAddAsync(0); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockLogger.VerifyFinishOperation(0); + _fixture.MockLogger.VerifyOperationFailed(0); + _fixture.MockRepository.VerifyAddAsync(0); } [Fact(DisplayName = nameof(GivenAValidRequestWhenRepositoryFailsThenFails))] public async Task GivenAValidRequestWhenRepositoryFailsThenFails() { // Arrange - var request = _fixture.SetValidRequest(); + var request = CreateNotificationUseCaseFixture.SetValidRequest(); _fixture.SetSuccessfulValidator(request); - _fixture.mockRepository.SetFailedAddAsync(); + _fixture.MockRepository.SetFailedAddAsync(); // Act - await _fixture.useCase.HandleAsync(request, _fixture.cancellationToken); + await _fixture.UseCase.HandleAsync(request, _fixture.CancellationToken); // Assert - _fixture.VerifyStartUseCaseLog(); - _fixture.VerifyFinishUseCaseLog(); - _fixture.VerifyFailedToCreateNotificationLog(1); - _fixture.mockRepository.VerifyAddAsync(1); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockLogger.VerifyOperationFailed(); + _fixture.MockLogger.VerifyFinishOperation(); + _fixture.MockRepository.VerifyAddAsync(1); } } diff --git a/templates/tests/UnitTests/Application/Notifications/GetAllNotificationsUseCaseTests.cs b/templates/Full/tests/UnitTests/Application/Notifications/GetAllNotificationsUseCaseTests.cs similarity index 62% rename from templates/tests/UnitTests/Application/Notifications/GetAllNotificationsUseCaseTests.cs rename to templates/Full/tests/UnitTests/Application/Notifications/GetAllNotificationsUseCaseTests.cs index 95f64652..17e6d410 100644 --- a/templates/tests/UnitTests/Application/Notifications/GetAllNotificationsUseCaseTests.cs +++ b/templates/Full/tests/UnitTests/Application/Notifications/GetAllNotificationsUseCaseTests.cs @@ -1,7 +1,6 @@ using Application.Common.Requests; using Application.Notifications; using Domain.Notifications; -using Microsoft.Extensions.Logging; using UnitTests.Application.Common; namespace UnitTests.Application.Notifications; @@ -10,14 +9,11 @@ public sealed class GetAllNotificationsUseCaseFixture : BaseApplicationFixture + public static new BasePaginatedRequest SetValidBasePaginatedRequest() => new(Guid.NewGuid(), 1, 10); - - public void VerifyNoNotificationsFoundLog(int times = 1) => - mockLogger.VerifyLog(l => l.LogWarning("*No notifications found.*"), Times.Exactly(times)); } public sealed class GetAllNotificationsUseCaseTests : IClassFixture @@ -35,14 +31,14 @@ public async Task GivenAValidRequestThenPass() { // Arrange var totalRecords = 5; - var request = _fixture.SetValidBasePaginatedRequest(); + var request = GetAllNotificationsUseCaseFixture.SetValidBasePaginatedRequest(); _fixture.SetSuccessfulValidator(request); - var expectedNotifications = _fixture.autoFixture.CreateMany(totalRecords); + var expectedNotifications = _fixture.AutoFixture.CreateMany(totalRecords); - _fixture.mockRepository.SetValidGetAllPaginatedAsyncNoIncludes(expectedNotifications, totalRecords); + _fixture.MockRepository.SetValidGetAllPaginatedAsyncNoIncludes(expectedNotifications, totalRecords); // Act - var result = await _fixture.useCase.HandleAsync(request, _fixture.cancellationToken); + var result = await _fixture.UseCase.HandleAsync(request, _fixture.CancellationToken); // Assert Assert.NotNull(result); @@ -53,41 +49,41 @@ public async Task GivenAValidRequestThenPass() Assert.Equal(1, result.TotalPages); Assert.Equal(totalRecords, result.TotalRecords); - _fixture.VerifyStartUseCaseLog(); - _fixture.VerifyNoNotificationsFoundLog(0); - _fixture.VerifyFinishUseCaseLog(); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockLogger.VerifyNotFound(0); + _fixture.MockLogger.VerifyFinishOperation(); } [Fact(DisplayName = nameof(GivenAnInvalidRequestThenFails))] public async Task GivenAnInvalidRequestThenFails() { // Arrange - var request = _fixture.SetValidBasePaginatedRequest(); + var request = GetAllNotificationsUseCaseFixture.SetValidBasePaginatedRequest(); _fixture.SetFailedValidator(request); // Act - var result = await _fixture.useCase.HandleAsync(request, _fixture.cancellationToken); + var result = await _fixture.UseCase.HandleAsync(request, _fixture.CancellationToken); // Assert Assert.False(result.Success); Assert.NotNull(result.Message); Assert.NotEmpty(result.Message); - _fixture.VerifyStartUseCaseLog(); - _fixture.VerifyNoNotificationsFoundLog(0); - _fixture.VerifyFinishUseCaseLog(0); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockLogger.VerifyNotFound(0); + _fixture.MockLogger.VerifyFinishOperation(0); } [Fact(DisplayName = nameof(GivenAValidRequestWhenNoNotificationsFoundThenFails))] public async Task GivenAValidRequestWhenNoNotificationsFoundThenFails() { // Arrange - var request = _fixture.SetValidBasePaginatedRequest(); + var request = GetAllNotificationsUseCaseFixture.SetValidBasePaginatedRequest(); _fixture.SetSuccessfulValidator(request); - _fixture.mockRepository.SetInvalidGetAllPaginatedAsync(); + _fixture.MockRepository.SetInvalidGetAllPaginatedAsync(); // Act - var result = await _fixture.useCase.HandleAsync(request, _fixture.cancellationToken); + var result = await _fixture.UseCase.HandleAsync(request, _fixture.CancellationToken); // Assert Assert.False(result.Success); @@ -95,8 +91,8 @@ public async Task GivenAValidRequestWhenNoNotificationsFoundThenFails() Assert.NotEmpty(result.Message); Assert.Equal("No notifications found.", result.Message); - _fixture.VerifyStartUseCaseLog(); - _fixture.VerifyNoNotificationsFoundLog(1); - _fixture.VerifyFinishUseCaseLog(); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockLogger.VerifyNotFound(1); + _fixture.MockLogger.VerifyFinishOperation(); } } diff --git a/templates/tests/UnitTests/Application/Notifications/GetNotificationUseCaseTest.cs b/templates/Full/tests/UnitTests/Application/Notifications/GetNotificationUseCaseTest.cs similarity index 63% rename from templates/tests/UnitTests/Application/Notifications/GetNotificationUseCaseTest.cs rename to templates/Full/tests/UnitTests/Application/Notifications/GetNotificationUseCaseTest.cs index 8e642927..3fa32f0c 100644 --- a/templates/tests/UnitTests/Application/Notifications/GetNotificationUseCaseTest.cs +++ b/templates/Full/tests/UnitTests/Application/Notifications/GetNotificationUseCaseTest.cs @@ -1,6 +1,5 @@ using Application.Notifications; using Domain.Notifications; -using Microsoft.Extensions.Logging; using UnitTests.Application.Common; namespace UnitTests.Application.Notifications; @@ -9,14 +8,11 @@ public sealed class GetNotificationUseCaseFixture : BaseApplicationFixture - new(Guid.NewGuid(), Math.Abs(autoFixture.Create()) + 1); - - public void VerifyNotificationNotFoundLog(int times = 1) => - mockLogger.VerifyLog(l => l.LogWarning("*Notification not found.*"), Times.Exactly(times)); + new(Guid.NewGuid(), Math.Abs(AutoFixture.Create()) + 1); } public sealed class GetNotificationUseCaseTests : IClassFixture @@ -35,11 +31,13 @@ public async Task GivenAValidRequestThenPass() // Arrange var request = _fixture.SetValidRequest(); _fixture.SetSuccessfulValidator(request); - var expectedNotification = _fixture.autoFixture.Create(); - _fixture.mockRepository.SetupGetByIdAsNoTrackingAsync(expectedNotification); + var expectedNotification = _fixture.AutoFixture.Build() + .With(n => n.Id, request.Id) + .Create(); + _fixture.MockRepository.SetupQueryable([expectedNotification]); // Act - var result = await _fixture.useCase.HandleAsync(request, _fixture.cancellationToken); + var result = await _fixture.UseCase.HandleAsync(request, _fixture.CancellationToken); // Assert Assert.NotNull(result); @@ -50,9 +48,10 @@ public async Task GivenAValidRequestThenPass() Assert.Equal(expectedNotification.NotificationType, result.Data.NotificationType); Assert.Equal(expectedNotification.NotificationStatus, result.Data.NotificationStatus); - _fixture.VerifyStartUseCaseLog(); - _fixture.VerifyNotificationNotFoundLog(0); - _fixture.VerifyFinishUseCaseLog(); + _fixture.MockRepository.VerifyQueryable(); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockLogger.VerifyNotFound(0); + _fixture.MockLogger.VerifyFinishOperation(); } [Fact(DisplayName = nameof(GivenAnInvalidRequestThenFails))] @@ -63,16 +62,17 @@ public async Task GivenAnInvalidRequestThenFails() _fixture.SetFailedValidator(request); // Act - var result = await _fixture.useCase.HandleAsync(request, _fixture.cancellationToken); + var result = await _fixture.UseCase.HandleAsync(request, _fixture.CancellationToken); // Assert Assert.False(result.Success); Assert.NotNull(result.Message); Assert.NotEmpty(result.Message); - _fixture.VerifyStartUseCaseLog(); - _fixture.VerifyNotificationNotFoundLog(0); - _fixture.VerifyFinishUseCaseLog(0); + _fixture.MockRepository.VerifyQueryable(0); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockLogger.VerifyNotFound(0); + _fixture.MockLogger.VerifyFinishOperation(0); } [Fact(DisplayName = nameof(GivenAValidRequestWhenNotificationNotFoundThenFails))] @@ -81,9 +81,10 @@ public async Task GivenAValidRequestWhenNotificationNotFoundThenFails() // Arrange var request = _fixture.SetValidRequest(); _fixture.SetSuccessfulValidator(request); + _fixture.MockRepository.SetupQueryable([]); // Act - var result = await _fixture.useCase.HandleAsync(request, _fixture.cancellationToken); + var result = await _fixture.UseCase.HandleAsync(request, _fixture.CancellationToken); // Assert Assert.False(result.Success); @@ -91,8 +92,9 @@ public async Task GivenAValidRequestWhenNotificationNotFoundThenFails() Assert.NotEmpty(result.Message); Assert.Equal("Notification not found.", result.Message); - _fixture.VerifyStartUseCaseLog(); - _fixture.VerifyNotificationNotFoundLog(1); - _fixture.VerifyFinishUseCaseLog(); + _fixture.MockRepository.VerifyQueryable(); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockLogger.VerifyNotFound(1); + _fixture.MockLogger.VerifyFinishOperation(); } } diff --git a/templates/tests/UnitTests/Application/Orders/CreateOrderUseCaseTests.cs b/templates/Full/tests/UnitTests/Application/Orders/CreateOrderUseCaseTests.cs similarity index 50% rename from templates/tests/UnitTests/Application/Orders/CreateOrderUseCaseTests.cs rename to templates/Full/tests/UnitTests/Application/Orders/CreateOrderUseCaseTests.cs index 4dd34cb1..09f39b8e 100644 --- a/templates/tests/UnitTests/Application/Orders/CreateOrderUseCaseTests.cs +++ b/templates/Full/tests/UnitTests/Application/Orders/CreateOrderUseCaseTests.cs @@ -3,16 +3,15 @@ using Domain.Orders; using FluentValidation; using FluentValidation.TestHelper; -using Microsoft.Extensions.Logging; using UnitTests.Application.Common; namespace UnitTests.Application.Orders; public sealed class CreateOrderRequestValidationFixture { - public IValidator validator = new CreateOrderRequestValidator(); + public IValidator Validator { get; } = new CreateOrderRequestValidator(); - public CreateOrderRequest GetValidRequest() => new(Guid.NewGuid(), "new order", [ + public static CreateOrderRequest GetValidRequest() => new(Guid.NewGuid(), "new order", [ new("item1", "description1", 10.0m), new("item2", "description2", 20.0m) ]); @@ -22,31 +21,31 @@ public sealed class CreateOrderRequestValidationTests(CreateOrderRequestValidati { private readonly CreateOrderRequestValidationFixture _fixture = fixture; - [Fact(DisplayName = nameof(Given_A_Valid_Request_Then_Pass))] - public async Task Given_A_Valid_Request_Then_Pass() + [Fact(DisplayName = nameof(GivenAValidRequestThenPass))] + public async Task GivenAValidRequestThenPass() { // Arrange - var request = _fixture.GetValidRequest(); + var request = CreateOrderRequestValidationFixture.GetValidRequest(); // Act - var result = await _fixture.validator.TestValidateAsync(request); + var result = await _fixture.Validator.TestValidateAsync(request); // Assert result.ShouldNotHaveAnyValidationErrors(); } - [Fact(DisplayName = nameof(Given_A_Invalid_Request_Then_Fails))] - public async Task Given_A_Invalid_Request_Then_Fails() + [Fact(DisplayName = nameof(GivenAnInvalidRequestThenFails))] + public async Task GivenAnInvalidRequestThenFails() { // Arrange - var request = _fixture.GetValidRequest() with + var request = CreateOrderRequestValidationFixture.GetValidRequest() with { CorrelationId = Guid.Empty, Description = string.Empty, Items = [] }; // Act - var result = await _fixture.validator.TestValidateAsync(request); + var result = await _fixture.Validator.TestValidateAsync(request); // Assert result.ShouldHaveValidationErrorFor("CorrelationId"); @@ -57,27 +56,18 @@ public async Task Given_A_Invalid_Request_Then_Fails() public sealed class CreateOrderUseCaseFixture : BaseApplicationFixture { - public CreateOrderUseCaseFixture() - { - useCase = new(mockServiceProvider.Object); - } + public CreateOrderUseCaseFixture() => UseCase = new(MockServiceProvider.Object); public CreateOrderRequest SetValidRequest() { - var items = autoFixture + var items = AutoFixture .CreateMany(1); - return new CreateOrderRequest(Guid.NewGuid(), "AwesomeComputer", [.. items]); + return new(Guid.NewGuid(), "AwesomeComputer", [.. items]); } public static CreateOrderRequest SetInvalidRequestWithNoItems() => new(Guid.NewGuid(), "AwesomeComputer", []); - - public void VerifyCreateOrderLogNoItemsError(int times = 1) => - mockLogger.VerifyLog(l => l.LogWarning("*Order must have at least one item.*"), Times.Exactly(times)); - - public void VerifyFailedToCreateOrderLog(int times = 1) => - mockLogger.VerifyLog(l => l.LogWarning("*Failed to create order.*"), Times.Exactly(times)); } public sealed class CreateOrderUseCaseTest : IClassFixture @@ -90,41 +80,41 @@ public CreateOrderUseCaseTest(CreateOrderUseCaseFixture fixture) _fixture.ClearInvocations(); } - [Fact(DisplayName = nameof(Given_A_Valid_Request_Then_Pass))] - public async Task Given_A_Valid_Request_Then_Pass() + [Fact(DisplayName = nameof(GivenAValidRequestThenPass))] + public async Task GivenAValidRequestThenPass() { // Arrange var request = _fixture.SetValidRequest(); _fixture.SetSuccessfulValidator(request); - _fixture.mockRepository.SetSuccessfulAddAsync(); + _fixture.MockRepository.SetSuccessfulAddAsync(); // Act - var result = await _fixture.useCase.HandleAsync(request, _fixture.cancellationToken); + var result = await _fixture.UseCase.HandleAsync(request, _fixture.CancellationToken); // Assert Assert.NotNull(result); Assert.True(result.Success); Assert.Null(result.Message); - _fixture.VerifyStartUseCaseLog(); - _fixture.VerifyFinishUseCaseLog(); - _fixture.VerifyCreateOrderLogNoItemsError(0); - _fixture.VerifyFailedToCreateOrderLog(0); - _fixture.mockRepository.VerifyAddAsync(1); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockLogger.VerifyFinishOperation(); + _fixture.MockLogger.VerifyWarning("Order must have at least one item.", 0); + _fixture.MockLogger.VerifyWarning("Failed to create order.", 0); + _fixture.MockRepository.VerifyAddAsync(1); _fixture.VerifyProduce(); } - [Fact(DisplayName = nameof(Given_A_Invalid_Request_Then_Fails))] - public async Task Given_A_Invalid_Request_Then_Fails() + [Fact(DisplayName = nameof(GivenAnInvalidRequestThenFails))] + public async Task GivenAnInvalidRequestThenFails() { // Arrange var request = _fixture.SetValidRequest(); _fixture.SetFailedValidator(request); // Act - var result = await _fixture.useCase.HandleAsync( + var result = await _fixture.UseCase.HandleAsync( request, - _fixture.cancellationToken + _fixture.CancellationToken ); // Assert @@ -132,25 +122,25 @@ public async Task Given_A_Invalid_Request_Then_Fails() Assert.NotNull(result.Message); Assert.NotEmpty(result.Message); - _fixture.VerifyStartUseCaseLog(); - _fixture.VerifyFinishUseCaseLog(0); - _fixture.VerifyCreateOrderLogNoItemsError(0); - _fixture.VerifyFailedToCreateOrderLog(0); - _fixture.mockRepository.VerifyAddAsync(0); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockLogger.VerifyFinishOperation(0); + _fixture.MockLogger.VerifyWarning("Order must have at least one item.", 0); + _fixture.MockLogger.VerifyWarning("Failed to create order.", 0); + _fixture.MockRepository.VerifyAddAsync(0); _fixture.VerifyProduce(0); } - [Fact(DisplayName = nameof(Given_A_Invalid_Request_Then_Fails_When_There_Is_No_Items))] - public async Task Given_A_Invalid_Request_Then_Fails_When_There_Is_No_Items() + [Fact(DisplayName = nameof(GivenAInvalidRequestThenFailsWhenThereIsNoItems))] + public async Task GivenAInvalidRequestThenFailsWhenThereIsNoItems() { // Arrange var request = CreateOrderUseCaseFixture.SetInvalidRequestWithNoItems(); _fixture.SetSuccessfulValidator(request); // Act - var result = await _fixture.useCase.HandleAsync( + var result = await _fixture.UseCase.HandleAsync( request, - _fixture.cancellationToken + _fixture.CancellationToken ); // Assert @@ -159,26 +149,26 @@ public async Task Given_A_Invalid_Request_Then_Fails_When_There_Is_No_Items() Assert.NotEmpty(result.Message); Assert.Equal("Order must have at least one item.", result.Message); - _fixture.VerifyStartUseCaseLog(); - _fixture.VerifyCreateOrderLogNoItemsError(1); - _fixture.VerifyFailedToCreateOrderLog(0); - _fixture.mockRepository.VerifyAddAsync(0); - _fixture.VerifyFinishUseCaseLog(); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockLogger.VerifyFinishOperation(); + _fixture.MockLogger.VerifyWarning("Order must have at least one item.", 1); + _fixture.MockLogger.VerifyWarning("Failed to create order.", 0); + _fixture.MockRepository.VerifyAddAsync(0); _fixture.VerifyProduce(); } - [Fact(DisplayName = nameof(Given_A_Valid_Request_Then_Fails_When_Repository_Returns_Zero))] - public async Task Given_A_Valid_Request_Then_Fails_When_Repository_Returns_Zero() + [Fact(DisplayName = nameof(GivenAValidRequestThenFailsWhenRepositoryReturnsZero))] + public async Task GivenAValidRequestThenFailsWhenRepositoryReturnsZero() { // Arrange var request = _fixture.SetValidRequest(); _fixture.SetSuccessfulValidator(request); - _fixture.mockRepository.SetFailedAddAsync(); + _fixture.MockRepository.SetFailedAddAsync(); // Act - var result = await _fixture.useCase.HandleAsync( + var result = await _fixture.UseCase.HandleAsync( request, - _fixture.cancellationToken + _fixture.CancellationToken ); // Assert @@ -187,11 +177,11 @@ public async Task Given_A_Valid_Request_Then_Fails_When_Repository_Returns_Zero( Assert.NotEmpty(result.Message); Assert.Equal("Failed to create order.", result.Message); - _fixture.VerifyStartUseCaseLog(); - _fixture.mockRepository.VerifyAddAsync(1); - _fixture.VerifyCreateOrderLogNoItemsError(0); - _fixture.VerifyFailedToCreateOrderLog(1); - _fixture.VerifyFinishUseCaseLog(); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockLogger.VerifyFinishOperation(); + _fixture.MockLogger.VerifyWarning("Order must have at least one item.", 0); + _fixture.MockLogger.VerifyWarning("Failed to create order.", 1); + _fixture.MockRepository.VerifyAddAsync(1); _fixture.VerifyProduce(); } } diff --git a/templates/tests/UnitTests/Application/Orders/GetAllOrdersUseCaseTest.cs b/templates/Full/tests/UnitTests/Application/Orders/GetAllOrdersUseCaseTest.cs similarity index 52% rename from templates/tests/UnitTests/Application/Orders/GetAllOrdersUseCaseTest.cs rename to templates/Full/tests/UnitTests/Application/Orders/GetAllOrdersUseCaseTest.cs index 6af4de04..7a6dbad7 100644 --- a/templates/tests/UnitTests/Application/Orders/GetAllOrdersUseCaseTest.cs +++ b/templates/Full/tests/UnitTests/Application/Orders/GetAllOrdersUseCaseTest.cs @@ -1,20 +1,13 @@ using Application.Common.Requests; using Application.Orders; using Domain.Orders; -using Microsoft.Extensions.Logging; using UnitTests.Application.Common; namespace UnitTests.Application.Orders; public sealed class GetAllOrdersUseCaseFixture : BaseApplicationFixture { - public GetAllOrdersUseCaseFixture() - { - useCase = new(mockServiceProvider.Object); - } - - public void VerifyNoOrdersFoundLog(int times = 1) => - mockLogger.VerifyLog(l => l.LogWarning("*No orders found.*"), Times.Exactly(times)); + public GetAllOrdersUseCaseFixture() => UseCase = new(MockServiceProvider.Object); } public sealed class GetAllOrdersUseCaseTest : IClassFixture @@ -27,19 +20,19 @@ public GetAllOrdersUseCaseTest(GetAllOrdersUseCaseFixture fixture) _fixture.ClearInvocations(); } - [Fact(DisplayName = nameof(Given_A_Valid_Request_Then_Pass))] - public async Task Given_A_Valid_Request_Then_Pass() + [Fact(DisplayName = nameof(GivenAValidRequestThenPass))] + public async Task GivenAValidRequestThenPass() { // Arrange var totalRecords = 5; var request = _fixture.SetValidBasePaginatedRequest(); _fixture.SetSuccessfulValidator(request); - var expectedOrders = _fixture.autoFixture.CreateMany(totalRecords); + var expectedOrders = _fixture.AutoFixture.CreateMany(totalRecords); - _fixture.mockRepository.SetValidGetAllPaginatedAsyncNoIncludes(expectedOrders, totalRecords); + _fixture.MockRepository.SetValidGetAllPaginatedAsyncNoIncludes(expectedOrders, totalRecords); // Act - var result = await _fixture.useCase.HandleAsync(request, _fixture.cancellationToken); + var result = await _fixture.UseCase.HandleAsync(request, _fixture.CancellationToken); // Assert Assert.NotNull(result); @@ -50,22 +43,22 @@ public async Task Given_A_Valid_Request_Then_Pass() Assert.Equal(1, result.TotalPages); Assert.Equal(totalRecords, result.TotalRecords); - _fixture.VerifyStartUseCaseLog(); - _fixture.mockRepository.VerifyGetAllPaginatedNoIncludes(1); - _fixture.VerifyNoOrdersFoundLog(0); - _fixture.VerifyFinishUseCaseLog(); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockRepository.VerifyGetAllPaginatedNoIncludes(1); + _fixture.MockLogger.VerifyNotFound(0); + _fixture.MockLogger.VerifyFinishOperation(); } - [Fact(DisplayName = nameof(Given_A_Valid_Request_When_No_Orders_Found_Then_Fails))] - public async Task Given_A_Valid_Request_When_No_Orders_Found_Then_Fails() + [Fact(DisplayName = nameof(GivenAValidRequestWhenNoOrdersFoundThenFails))] + public async Task GivenAValidRequestWhenNoOrdersFoundThenFails() { // Arrange var request = _fixture.SetValidBasePaginatedRequest(); _fixture.SetSuccessfulValidator(request); - _fixture.mockRepository.SetInvalidGetAllPaginatedAsync(); + _fixture.MockRepository.SetInvalidGetAllPaginatedAsync(); // Act - var result = await _fixture.useCase.HandleAsync(request, _fixture.cancellationToken); + var result = await _fixture.UseCase.HandleAsync(request, _fixture.CancellationToken); // Assert Assert.False(result.Success); @@ -73,30 +66,30 @@ public async Task Given_A_Valid_Request_When_No_Orders_Found_Then_Fails() Assert.NotEmpty(result.Message); Assert.Equal("No orders found.", result.Message); - _fixture.VerifyStartUseCaseLog(); - _fixture.mockRepository.VerifyGetAllPaginatedNoIncludes(1); - _fixture.VerifyNoOrdersFoundLog(1); - _fixture.VerifyFinishUseCaseLog(); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockRepository.VerifyGetAllPaginatedNoIncludes(1); + _fixture.MockLogger.VerifyNotFound(1); + _fixture.MockLogger.VerifyFinishOperation(); } - [Fact(DisplayName = nameof(Given_A_Invalid_Request_Then_Fails))] - public async Task Given_A_Invalid_Request_Then_Fails() + [Fact(DisplayName = nameof(GivenAnInvalidRequestThenFails))] + public async Task GivenAnInvalidRequestThenFails() { // Arrange var request = _fixture.SetValidBasePaginatedRequest(); _fixture.SetFailedValidator(request); // Act - var result = await _fixture.useCase.HandleAsync(request, _fixture.cancellationToken); + var result = await _fixture.UseCase.HandleAsync(request, _fixture.CancellationToken); // Assert Assert.False(result.Success); Assert.NotNull(result.Message); Assert.NotEmpty(result.Message); - _fixture.VerifyStartUseCaseLog(); - _fixture.mockRepository.VerifyGetAllPaginatedNoIncludes(0); - _fixture.VerifyNoOrdersFoundLog(0); - _fixture.VerifyFinishUseCaseLog(0); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockRepository.VerifyGetAllPaginatedNoIncludes(0); + _fixture.MockLogger.VerifyNotFound(0); + _fixture.MockLogger.VerifyFinishOperation(0); } } diff --git a/templates/Full/tests/UnitTests/Application/Orders/GetOrderUseCaseTests.cs b/templates/Full/tests/UnitTests/Application/Orders/GetOrderUseCaseTests.cs new file mode 100644 index 00000000..0fc61235 --- /dev/null +++ b/templates/Full/tests/UnitTests/Application/Orders/GetOrderUseCaseTests.cs @@ -0,0 +1,103 @@ +using Application.Orders; +using Domain.Orders; +using UnitTests.Application.Common; + +namespace UnitTests.Application.Orders; + +public sealed class GetOrderUseCaseFixture : BaseApplicationFixture +{ + public GetOrderUseCaseFixture() => UseCase = new(MockServiceProvider.Object); + public GetOrderRequest SetValidRequest(int? id = null) => new(Guid.NewGuid(), id ?? AutoFixture.Create()); +} + +public sealed class GetOrderUseCaseTest : IClassFixture +{ + private readonly GetOrderUseCaseFixture _fixture; + + public GetOrderUseCaseTest(GetOrderUseCaseFixture fixture) + { + _fixture = fixture; + _fixture.ClearInvocations(); + } + + [Fact(DisplayName = nameof(GivenAValidRequestThenPass))] + public async Task GivenAValidRequestThenPass() + { + // Arrange + var resultCreateOrder = Order.Create( + "Test Order", + [ + new("Item 1", "Description 1", 500m), + new("Item 2", "Description 2", 500m) + ] + ); + var expectedOrder = resultCreateOrder.Value; + var request = _fixture.SetValidRequest(resultCreateOrder.Value.Id); + request = request with { Id = expectedOrder.Id }; + _fixture.SetSuccessfulValidator(request); + _fixture.MockRepository.SetupQueryable(request.CorrelationId, null, [expectedOrder]); + + // Act + var result = await _fixture.UseCase.HandleAsync(request, _fixture.CancellationToken); + + // Assert + Assert.NotNull(result); + Assert.True(result.Success); + Assert.Null(result.Message); + Assert.NotNull(result.Data); + Assert.Equal(expectedOrder.Id, result.Data.Id); + Assert.Equal(expectedOrder.Description, result.Data.Description); + Assert.Equal(expectedOrder.Total, result.Data.Total); + Assert.NotNull(result.Data.Items); + Assert.Equal(expectedOrder.Items?.Count, result.Data.Items!.Count); + + _fixture.MockRepository.VerifyQueryable(); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockLogger.VerifyNotFound(0); + _fixture.MockLogger.VerifyFinishOperation(); + } + + [Fact(DisplayName = nameof(GivenAInvalidRequestThenFails))] + public async Task GivenAInvalidRequestThenFails() + { + // Arrange + var request = _fixture.SetValidRequest(); + _fixture.SetFailedValidator(request); + + // Act + var result = await _fixture.UseCase.HandleAsync(request, _fixture.CancellationToken); + + // Assert + Assert.False(result.Success); + Assert.NotNull(result.Message); + Assert.NotEmpty(result.Message); + + _fixture.MockRepository.VerifyQueryable(0); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockLogger.VerifyNotFound(0); + _fixture.MockLogger.VerifyFinishOperation(0); + } + + [Fact(DisplayName = nameof(GivenAValidRequestWhenOrderNotFoundThenFails))] + public async Task GivenAValidRequestWhenOrderNotFoundThenFails() + { + // Arrange + var request = _fixture.SetValidRequest(); + _fixture.SetSuccessfulValidator(request); + _fixture.MockRepository.SetupQueryable(request.CorrelationId, null, []); + + // Act + var result = await _fixture.UseCase.HandleAsync(request, _fixture.CancellationToken); + + // Assert + Assert.False(result.Success); + Assert.NotNull(result.Message); + Assert.NotEmpty(result.Message); + Assert.Equal("Order not found.", result.Message); + + _fixture.MockRepository.VerifyQueryable(); + _fixture.MockLogger.VerifyStartOperation(); + _fixture.MockLogger.VerifyNotFound(1); + _fixture.MockLogger.VerifyFinishOperation(); + } +} diff --git a/templates/tests/UnitTests/Architecture/ApplicationTests.cs b/templates/Full/tests/UnitTests/Architecture/ApplicationTests.cs similarity index 74% rename from templates/tests/UnitTests/Architecture/ApplicationTests.cs rename to templates/Full/tests/UnitTests/Architecture/ApplicationTests.cs index 4fd71868..4bd3ac0a 100644 --- a/templates/tests/UnitTests/Architecture/ApplicationTests.cs +++ b/templates/Full/tests/UnitTests/Architecture/ApplicationTests.cs @@ -9,13 +9,13 @@ public sealed class ApplicationTests { private static readonly Assembly _applicationAssembly = typeof(BaseResponse).Assembly; - [Theory(DisplayName = nameof(Application_Do_Not_Have_Classes_With_Not_Allowed_Names))] + [Theory(DisplayName = nameof(ApplicationDoNotHaveClassesWithNotAllowedNames))] [InlineData("Entity")] [InlineData("ValueObject")] [InlineData("Vo")] [InlineData("Service")] [InlineData("Controller")] - public void Application_Do_Not_Have_Classes_With_Not_Allowed_Names(string notAllowedClassName) + public void ApplicationDoNotHaveClassesWithNotAllowedNames(string notAllowedClassName) { // Arrange, Act var result = Types @@ -30,8 +30,8 @@ public void Application_Do_Not_Have_Classes_With_Not_Allowed_Names(string notAll Assert.True(result.IsSuccessful); } - [Fact(DisplayName = nameof(Application_Do_Not_Have_Infrastructure_Dependency))] - public void Application_Do_Not_Have_Infrastructure_Dependency() + [Fact(DisplayName = nameof(ApplicationDoNotHaveInfrastructureDependency))] + public void ApplicationDoNotHaveInfrastructureDependency() { // Arrange, Act var result = Types @@ -44,8 +44,8 @@ public void Application_Do_Not_Have_Infrastructure_Dependency() Assert.True(result.IsSuccessful); } - [Fact(DisplayName = nameof(Application_Should_Has_Valid_Scopes))] - public void Application_Should_Has_Valid_Scopes() + [Fact(DisplayName = nameof(ApplicationShouldHasValidScopes))] + public void ApplicationShouldHasValidScopes() { // Arrange ServiceCollection serviceCollection = new(); diff --git a/templates/tests/UnitTests/Architecture/DomainTests.cs b/templates/Full/tests/UnitTests/Architecture/DomainTests.cs similarity index 75% rename from templates/tests/UnitTests/Architecture/DomainTests.cs rename to templates/Full/tests/UnitTests/Architecture/DomainTests.cs index 2b747788..b3438e99 100644 --- a/templates/tests/UnitTests/Architecture/DomainTests.cs +++ b/templates/Full/tests/UnitTests/Architecture/DomainTests.cs @@ -9,7 +9,7 @@ public sealed class DomainTests { private static readonly Assembly _domainAssembly = typeof(Result).Assembly; - [Theory(DisplayName = nameof(Domain_Do_Not_Have_Classes_With_Not_Allowed_Names))] + [Theory(DisplayName = nameof(DomainDoNotHaveClassesWithNotAllowedNames))] [InlineData("Dto")] [InlineData("Dtos")] [InlineData("UseCase")] @@ -21,7 +21,7 @@ public sealed class DomainTests [InlineData("Controller")] [InlineData("Repository")] [InlineData("Query")] - public void Domain_Do_Not_Have_Classes_With_Not_Allowed_Names(string notAllowedClassName) + public void DomainDoNotHaveClassesWithNotAllowedNames(string notAllowedClassName) { // Arrange, Act var result = Types @@ -36,8 +36,8 @@ public void Domain_Do_Not_Have_Classes_With_Not_Allowed_Names(string notAllowedC Assert.True(result.IsSuccessful); } - [Fact(DisplayName = nameof(Domain_Do_Not_Have_Application_Dependency))] - public void Domain_Do_Not_Have_Application_Dependency() + [Fact(DisplayName = nameof(DomainDoNotHaveApplicationDependency))] + public void DomainDoNotHaveApplicationDependency() { // Arrange, Act var result = Types @@ -50,8 +50,8 @@ public void Domain_Do_Not_Have_Application_Dependency() Assert.True(result.IsSuccessful); } - [Fact(DisplayName = nameof(Domain_Do_Not_Have_Infrastructure_Dependency))] - public void Domain_Do_Not_Have_Infrastructure_Dependency() + [Fact(DisplayName = nameof(DomainDoNotHaveInfrastructureDependency))] + public void DomainDoNotHaveInfrastructureDependency() { // Arrange, Act var result = Types @@ -64,8 +64,8 @@ public void Domain_Do_Not_Have_Infrastructure_Dependency() Assert.True(result.IsSuccessful); } - [Fact(DisplayName = nameof(Domain_Should_Has_Valid_Scopes))] - public void Domain_Should_Has_Valid_Scopes() + [Fact(DisplayName = nameof(DomainShouldHasValidScopes))] + public void DomainShouldHasValidScopes() { // Arrange ServiceCollection serviceCollection = new(); diff --git a/templates/Full/tests/UnitTests/Domain/DomainEntityTests.cs b/templates/Full/tests/UnitTests/Domain/DomainEntityTests.cs new file mode 100644 index 00000000..301ed049 --- /dev/null +++ b/templates/Full/tests/UnitTests/Domain/DomainEntityTests.cs @@ -0,0 +1,161 @@ +using Domain.Common; + +namespace UnitTests.Domain; + +public sealed class DomainEntityTests +{ + private sealed class TestDomainEntity : DomainEntity + { + public TestDomainEntity() : base() { } + public TestDomainEntity(string user, string timezoneId = "") + : base(user, timezoneId) { } + } + + [Fact(DisplayName = nameof(ConstructorWithValidParametersShouldCreateEntityWithProvidedValues))] + public void ConstructorWithValidParametersShouldCreateEntityWithProvidedValues() + { + var user = "TestUser"; + var timezoneId = "America/New_York"; + + var entity = new TestDomainEntity(user, timezoneId); + + Assert.Equal(user, entity.CreatedBy); + Assert.Equal(timezoneId, entity.CreatedByTimezoneId); + Assert.Equal(user, entity.UpdatedBy); + Assert.Equal(timezoneId, entity.UpdatedByTimezoneId); + } + + [Fact(DisplayName = nameof(ConstructorWithValidTimezoneIdShouldSetTimezoneId))] + public void ConstructorWithValidTimezoneIdShouldSetTimezoneId() + { + var timezoneId = "America/Sao_Paulo"; + + var entity = new TestDomainEntity("TestUser", timezoneId); + + Assert.Equal(timezoneId, entity.CreatedByTimezoneId); + Assert.Equal(timezoneId, entity.UpdatedByTimezoneId); + } + + [Fact(DisplayName = nameof(ConstructorWithInvalidTimezoneIdShouldDefaultToUtc))] + public void ConstructorWithInvalidTimezoneIdShouldDefaultToUtc() + { + var invalidTimezoneId = "Invalid/Timezone"; + + var result = Assert.Throws(() => new TestDomainEntity("TestUser", invalidTimezoneId)); + + Assert.Contains("not found on the local computer.", result.Message); + } + + [Fact(DisplayName = nameof(ConstructorWithNullTimezoneIdShouldDefaultToUtc))] + public void ConstructorWithNullTimezoneIdShouldDefaultToUtc() + { + string? nullTimezone = null; + + var entity = new TestDomainEntity("TestUser", nullTimezone!); + + Assert.Equal(TimeZoneInfo.Utc.Id, entity.CreatedByTimezoneId); + Assert.Equal(TimeZoneInfo.Utc.Id, entity.UpdatedByTimezoneId); + } + + [Fact(DisplayName = nameof(ConstructorWithEmptyTimezoneIdShouldDefaultToUtc))] + public void ConstructorWithEmptyTimezoneIdShouldDefaultToUtc() + { + var entity = new TestDomainEntity("TestUser", string.Empty); + + Assert.Equal(TimeZoneInfo.Utc.Id, entity.CreatedByTimezoneId); + Assert.Equal(TimeZoneInfo.Utc.Id, entity.UpdatedByTimezoneId); + } + + [Fact(DisplayName = nameof(ConstructorWithWhitespaceTimezoneIdShouldDefaultToUtc))] + public void ConstructorWithWhitespaceTimezoneIdShouldDefaultToUtc() + { + var entity = new TestDomainEntity("TestUser", " "); + + Assert.Equal(TimeZoneInfo.Utc.Id, entity.CreatedByTimezoneId); + Assert.Equal(TimeZoneInfo.Utc.Id, entity.UpdatedByTimezoneId); + } + + [Fact(DisplayName = nameof(UpdateWithValidParametersShouldUpdateEntityProperties))] + public void UpdateWithValidParametersShouldUpdateEntityProperties() + { + var entity = new TestDomainEntity("InitialUser", "America/New_York"); + var beforeUpdate = DateTime.UtcNow; + + entity.Update("UpdatedUser", "Europe/London"); + + var afterUpdate = DateTime.UtcNow; + Assert.True(entity.UpdatedAt >= beforeUpdate && entity.UpdatedAt <= afterUpdate); + Assert.Equal("UpdatedUser", entity.UpdatedBy); + Assert.Equal("Europe/London", entity.UpdatedByTimezoneId); + Assert.Equal("InitialUser", entity.CreatedBy); + } + + [Fact(DisplayName = nameof(UpdateWithNullUserShouldDefaultToSystem))] + public void UpdateWithNullUserShouldDefaultToSystem() + { + var entity = new TestDomainEntity("InitialUser"); + + entity.Update(null); + + Assert.Equal("System", entity.UpdatedBy); + } + + [Fact(DisplayName = nameof(UpdateWithInvalidTimezoneIdShouldDefaultToUtc))] + public void UpdateWithInvalidTimezoneIdShouldDefaultToUtc() + { + var entity = new TestDomainEntity("TestUser", "America/New_York"); + + var result = Assert.Throws(() => entity.Update("UpdatedUser", "Invalid/Timezone")); + + Assert.Contains("not found on the local computer.", result.Message); + } + + [Fact(DisplayName = nameof(UpdateWithEmptyTimezoneIdShouldDefaultToUtc))] + public void UpdateWithEmptyTimezoneIdShouldDefaultToUtc() + { + var entity = new TestDomainEntity("TestUser", "America/New_York"); + + entity.Update("UpdatedUser", string.Empty); + + Assert.Equal("America/New_York", entity.CreatedByTimezoneId); + Assert.Equal(TimeZoneInfo.Utc.Id, entity.UpdatedByTimezoneId); + } + + [Fact(DisplayName = nameof(UpdateWithoutParametersShouldUseDefaultValues))] + public void UpdateWithoutParametersShouldUseDefaultValues() + { + var entity = new TestDomainEntity("InitialUser", "America/New_York"); + var beforeUpdate = DateTime.UtcNow; + + entity.Update(); + + var afterUpdate = DateTime.UtcNow; + Assert.True(entity.UpdatedAt >= beforeUpdate && entity.UpdatedAt <= afterUpdate); + Assert.Equal("System", entity.UpdatedBy); + Assert.Equal(TimeZoneInfo.Utc.Id, entity.UpdatedByTimezoneId); + } + + [Fact(DisplayName = nameof(ConstructorWithUtcTimezoneIdShouldSetUtcTimezone))] + public void ConstructorWithUtcTimezoneIdShouldSetUtcTimezone() + { + var entity = new TestDomainEntity("TestUser", TimeZoneInfo.Utc.Id); + + Assert.Equal(TimeZoneInfo.Utc.Id, entity.CreatedByTimezoneId); + Assert.Equal(TimeZoneInfo.Utc.Id, entity.UpdatedByTimezoneId); + } + + [Fact(DisplayName = nameof(UpdatePreservesCreatedAtAndCreatedByWhenUpdating))] + public void UpdatePreservesCreatedAtAndCreatedByWhenUpdating() + { + var initialUser = "InitialUser"; + var entity = new TestDomainEntity(initialUser, "America/New_York"); + + entity.Update("UpdatedUser", "Europe/London"); + entity.Update("AnotherUser", "Asia/Tokyo"); + + Assert.Equal(initialUser, entity.CreatedBy); + Assert.Equal("AnotherUser", entity.UpdatedBy); + Assert.Equal("America/New_York", entity.CreatedByTimezoneId); + Assert.Equal("Asia/Tokyo", entity.UpdatedByTimezoneId); + } +} diff --git a/templates/tests/UnitTests/Domain/NotificationTests.cs b/templates/Full/tests/UnitTests/Domain/NotificationTests.cs similarity index 60% rename from templates/tests/UnitTests/Domain/NotificationTests.cs rename to templates/Full/tests/UnitTests/Domain/NotificationTests.cs index a577361e..ae60a658 100644 --- a/templates/tests/UnitTests/Domain/NotificationTests.cs +++ b/templates/Full/tests/UnitTests/Domain/NotificationTests.cs @@ -1,41 +1,45 @@ +using Domain.Common.Enums; using Domain.Notifications; namespace UnitTests.Domain; public sealed class NotificationTests { - [Fact(DisplayName = nameof(Given_A_New_Notification_When_Properties_Are_Provided_Then_Should_Create_Notification_With_Success))] - public void Given_A_New_Notification_When_Properties_Are_Provided_Then_Should_Create_Notification_With_Success() + [Fact(DisplayName = nameof(GivenANewNotificationWhenPropertiesAreProvidedThenShouldCreateNotificationWithSuccess))] + public void GivenANewNotificationWhenPropertiesAreProvidedThenShouldCreateNotificationWithSuccess() { /// Arrange - var notificationType = "TestNotification"; + var notificationType = NotificationType.OrderCreated; var notificationStatus = "Success"; var createdBy = "System"; + var timezoneId = "America/New_York"; var message = new { Test = "Message" }; /// Act - Notification notification = new(notificationType, notificationStatus, createdBy, message); + Notification notification = new(notificationType, notificationStatus, message, createdBy, timezoneId); // Assert Assert.NotNull(notification); Assert.Equal(notificationType, notification.NotificationType); Assert.Equal(notificationStatus, notification.NotificationStatus); Assert.Equal(createdBy, notification.CreatedBy); + Assert.Equal(timezoneId, notification.CreatedByTimezoneId); Assert.NotNull(notification.Message); Assert.Contains("\"Test\":\"Message\"", notification.Message); } - [Fact(DisplayName = nameof(Given_A_New_Notification_When_Message_Is_Null_Then_Should_Create_Notification_With_Empty_Message))] - public void Given_A_New_Notification_When_Message_Is_Null_Then_Should_Create_Notification_With_Empty_Message() + [Fact(DisplayName = nameof(GivenANewNotificationWhenMessageIsNullThenShouldCreateNotificationWithEmptyMessage))] + public void GivenANewNotificationWhenMessageIsNullThenShouldCreateNotificationWithEmptyMessage() { /// Arrange - var notificationType = "TestNotification"; + var notificationType = NotificationType.OrderCreated; var notificationStatus = "Success"; var createdBy = "System"; + var timezoneId = "America/New_York"; object? message = null; /// Act - Notification notification = new(notificationType, notificationStatus, createdBy, message); + Notification notification = new(notificationType, notificationStatus, message, createdBy, timezoneId); // Assert Assert.NotNull(notification); diff --git a/templates/Full/tests/UnitTests/Domain/OrderTests.cs b/templates/Full/tests/UnitTests/Domain/OrderTests.cs new file mode 100644 index 00000000..cba44ee0 --- /dev/null +++ b/templates/Full/tests/UnitTests/Domain/OrderTests.cs @@ -0,0 +1,98 @@ +using Domain.Common; +using Domain.Orders; + +namespace UnitTests.Domain; + +public sealed class OrderTests +{ + [Fact(DisplayName = nameof(GivenANewOrderWhenItemsAreProvidedThenShouldCreatedWithSuccess))] + public void GivenANewOrderWhenItemsAreProvidedThenShouldCreatedWithSuccess() + { + /// Arrange + var items = new List() + { + new("Computer", "Desktop", 900), + new("Mouse", "Razer", 100), + new("Headphone", "Logitech", 100), + }; + + /// Act + var result = Order.Create("Amazing Computer", items, "John Doe", "America/New_York"); + var order = result.Value; + var initialUpdatedAt = order.UpdatedAt; + + // Assert + Assert.NotNull(result); + Assert.True(result.Success); + Assert.Empty(result.Message); + Assert.IsType(result.Value); + + Assert.NotNull(order); + Assert.NotNull(result); + Assert.True(result.Success); + Assert.Empty(result.Message); + Assert.NotEqual(0, order.Total); + Assert.Equal("John Doe", order.CreatedBy); + Assert.Equal("America/New_York", order.CreatedByTimezoneId); + Assert.Equal(items.Sum(i => i.Value), order.Total); + } + + [Fact(DisplayName = nameof(GivenANewOrderWithoutUserAndTimezoneWhenItemsAreProvidedThenShouldCreateWithSuccess))] + public void GivenANewOrderWithoutUserAndTimezoneWhenItemsAreProvidedThenShouldCreateWithSuccess() + { + /// Arrange + var items = new List() + { + new("Computer", "Desktop", 900), + new("Mouse", "Razer", 100), + new("Headphone", "Logitech", 100), + }; + + /// Act + var result = Order.Create("Amazing Computer", items); + var order = result.Value; + var initialUpdatedAt = order.UpdatedAt; + + // Assert + Assert.NotNull(result); + Assert.True(result.Success); + Assert.Empty(result.Message); + Assert.IsType(result.Value); + + Assert.NotNull(order); + Assert.NotNull(result); + Assert.True(result.Success); + Assert.Empty(result.Message); + Assert.NotEqual(0, order.Total); + Assert.Equal(items.Sum(i => i.Value), order.Total); + Assert.Equal("System", order.CreatedBy); + Assert.Equal("UTC", order.CreatedByTimezoneId); + Assert.Equal("System", order.UpdatedBy); + Assert.Equal("UTC", order.UpdatedByTimezoneId); + } + + [Fact(DisplayName = nameof(GivenANewItemWithValueZeroThenShouldBeFailure))] + public void GivenANewItemWithValueZeroThenShouldBeFailure() + { + // Arrange, Act + var exception = Assert.Throws(() => new Item("Mouse", "Razer", 0)); + + // Assert + Assert.NotNull(exception); + Assert.Equal("Item value cannot be zero or negative.", exception.Message); + } + + + + [Fact(DisplayName = nameof(GivenANewOrderWhenItemsIsEmptyThenShouldReturnFailure))] + public void GivenANewOrderWhenItemsIsEmptyThenShouldReturnFailure() + { + /// Arrange, Act + var result = Order.Create("Amazing Computer", Array.Empty()); + + // Assert + Assert.NotNull(result); + Assert.True(result.IsFailure); + Assert.Equal("Order must have at least one item.", result.Message); + } +} diff --git a/templates/tests/UnitTests/GlobalUsings.cs b/templates/Full/tests/UnitTests/GlobalUsings.cs similarity index 100% rename from templates/tests/UnitTests/GlobalUsings.cs rename to templates/Full/tests/UnitTests/GlobalUsings.cs diff --git a/templates/tests/UnitTests/UnitTests.csproj b/templates/Full/tests/UnitTests/UnitTests.csproj similarity index 95% rename from templates/tests/UnitTests/UnitTests.csproj rename to templates/Full/tests/UnitTests/UnitTests.csproj index f7de22de..25373a55 100644 --- a/templates/tests/UnitTests/UnitTests.csproj +++ b/templates/Full/tests/UnitTests/UnitTests.csproj @@ -9,6 +9,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/templates/Full/tests/UnitTests/packages.lock.json b/templates/Full/tests/UnitTests/packages.lock.json new file mode 100644 index 00000000..98f491d9 --- /dev/null +++ b/templates/Full/tests/UnitTests/packages.lock.json @@ -0,0 +1,356 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[8.0.1, )", + "resolved": "8.0.1", + "contentHash": "heVQl5tKYnnIDYlR1QMVGueYH6iriZTcZB6AjDczQNwZzxkjDIt9C84Pt4cCiZYrbo7jkZOYGWbs6Lo9wAtVLg==" + }, + "ILogger.Moq": { + "type": "Direct", + "requested": "[2.0.0, )", + "resolved": "2.0.0", + "contentHash": "GY6xsOTwcxJY/PxK3blmX7OP+pnFmO/ngLQOy8NXn09+di1By9IVKyexvP6QlHL3c1rr0IH9lz/SZVZup2BjAQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "2.0.0", + "Moq": "4.18.0" + } + }, + "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" + } + }, + "MockQueryable.Moq": { + "type": "Direct", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "D6d2NvV0kShKih2OKINKnW7joRzfL+bdBjFNj0rw11EPp/3Mgqm5svwsvQ2UHDqqwgwavjQGfGL6zKB1iDJA3g==", + "dependencies": { + "MockQueryable.EntityFrameworkCore": "10.0.5", + "Moq": "4.20.72" + } + }, + "Moq": { + "type": "Direct", + "requested": "[4.20.72, )", + "resolved": "4.20.72", + "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "NetArchTest.Rules": { + "type": "Direct", + "requested": "[1.3.2, )", + "resolved": "1.3.2", + "contentHash": "puPyNXkwJq8/UwXhHV8NrzNzkQl4IxEbcP+3PU0xLRiOedsVpaSdpwHhvOZfI0VwTcRvawCNxYQcSRbD4RUg4w==", + "dependencies": { + "Mono.Cecil": "0.11.3" + } + }, + "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==" + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "dependencies": { + "System.Diagnostics.EventLog": "6.0.0" + } + }, + "Fare": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "HaI8puqA66YU7/9cK4Sgbs1taUTP1Ssa4QT2PIzqJ7GvAbN1QgkjbRsjH+FSbMh1MJdvS0CIwQNLtFT+KF6KpA==", + "dependencies": { + "NETStandard.Library": "1.6.1" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.3.0", + "contentHash": "23BNy/vziREC20Wwhb50K7+kZe0m07KlLWDQv4qjJ9tt3QjpDpDIqJFrhYHmMEo9xDkuSp55U/8h4bMF7MiB+g==" + }, + "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.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", + "dependencies": { + "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==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "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" + } + }, + "MockQueryable.Core": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8F5qLtCaXx9L2cBiq1ryF2McglfMDoDZbWFz4L+WaS7trf2i7KON57VMZAqWR7jmF7Nqoq69qrKKIVqUUHLdbw==" + }, + "MockQueryable.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "zQe2B4JQoX6/tRh+G3mkhvZZ+Hr4rrNU3HdwEYpgHGp4yJ6vtuvxIDJ/55DoCMXDE6WX2NmUpc4hkMggRg+69g==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "MockQueryable.Core": "10.0.5" + } + }, + "Mono.Cecil": { + "type": "Transitive", + "resolved": "0.11.3", + "contentHash": "DNYE+io5XfEE8+E+5padThTPHJARJHbz1mhbhMPNrrWGKVKKqj/KEeLvbawAmbIcT73NuxLV7itHZaYCZcVWGg==" + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" + }, + "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]" + } + }, + "application": { + "type": "Project", + "dependencies": { + "Domain": "[1.0.0, )", + "FluentValidation": "[12.1.1, )", + "FluentValidation.DependencyInjectionExtensions": "[12.1.1, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.Extensions.Logging": "[10.0.5, )" + } + }, + "commontests": { + "type": "Project", + "dependencies": { + "AutoFixture": "[4.18.1, )", + "Newtonsoft.Json": "[13.0.4, )", + "xunit.extensibility.core": "[2.9.3, )" + } + }, + "domain": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "[10.0.5, )" + } + }, + "AutoFixture": { + "type": "CentralTransitive", + "requested": "[4.18.1, )", + "resolved": "4.18.1", + "contentHash": "BmWZDY4fkrYOyd5/CTBOeXbzsNwV8kI4kDi/Ty1Y5F+WDHBVKxzfWlBE4RSicvZ+EOi2XDaN5uwdrHsItLW6Kw==", + "dependencies": { + "Fare": "[2.1.1, 3.0.0)" + } + }, + "FluentValidation": { + "type": "CentralTransitive", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" + }, + "FluentValidation.DependencyInjectionExtensions": { + "type": "CentralTransitive", + "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": "CentralTransitive", + "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.EntityFrameworkCore.Relational": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "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" + } + }, + "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==" + }, + "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/UnitTests/stryker-config-application.json b/templates/Full/tests/UnitTests/stryker-config-application.json similarity index 100% rename from templates/tests/UnitTests/stryker-config-application.json rename to templates/Full/tests/UnitTests/stryker-config-application.json diff --git a/templates/tests/UnitTests/stryker-config-domain.json b/templates/Full/tests/UnitTests/stryker-config-domain.json similarity index 100% rename from templates/tests/UnitTests/stryker-config-domain.json rename to templates/Full/tests/UnitTests/stryker-config-domain.json diff --git a/templates/Hexagonal.Solution.Template.sln b/templates/Hexagonal.Solution.Template.sln deleted file mode 100644 index 7988e323..00000000 --- a/templates/Hexagonal.Solution.Template.sln +++ /dev/null @@ -1,85 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 18 -VisualStudioVersion = 18.3.11222.16 d18.3 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6A878B68-ADCF-470F-8273-DA5C32E2DB79}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{D499CDEC-C117-4968-A519-21C64757C640}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain", "src\Domain\Domain.csproj", "{E2990ADA-8398-4769-98E9-EA105130137D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "src\Application\Application.csproj", "{3F9D43F2-9555-4771-A81B-9EBDE1A2C7CF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonTests", "tests\CommonTests\CommonTests.csproj", "{A1376FF6-1F7E-4D68-967A-121572141BA0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApp", "src\WebApp\WebApp.csproj", "{EB1974DC-D58F-41A8-A965-CC772F2B8CEB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "tests\UnitTests\UnitTests.csproj", "{5E16731E-1160-4F78-811C-037EE40C5AC0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "tests\IntegrationTests\IntegrationTests.csproj", "{EA853C22-3BDD-43C0-A150-8C77BBE41058}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{8C2C5A8B-5CC4-48B3-8266-EBFB68DFC51C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{34744AD9-DAE2-429F-94C2-CCE51DFAD280}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - .gitignore = .gitignore - Directory.Build.props = Directory.Build.props - Directory.Packages.props = Directory.Packages.props - docker-compose.yml = docker-compose.yml - scripts\migrations.sql = scripts\migrations.sql - scripts\seeds.sql = scripts\seeds.sql - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {E2990ADA-8398-4769-98E9-EA105130137D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E2990ADA-8398-4769-98E9-EA105130137D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E2990ADA-8398-4769-98E9-EA105130137D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E2990ADA-8398-4769-98E9-EA105130137D}.Release|Any CPU.Build.0 = Release|Any CPU - {3F9D43F2-9555-4771-A81B-9EBDE1A2C7CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3F9D43F2-9555-4771-A81B-9EBDE1A2C7CF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3F9D43F2-9555-4771-A81B-9EBDE1A2C7CF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3F9D43F2-9555-4771-A81B-9EBDE1A2C7CF}.Release|Any CPU.Build.0 = Release|Any CPU - {A1376FF6-1F7E-4D68-967A-121572141BA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A1376FF6-1F7E-4D68-967A-121572141BA0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1376FF6-1F7E-4D68-967A-121572141BA0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A1376FF6-1F7E-4D68-967A-121572141BA0}.Release|Any CPU.Build.0 = Release|Any CPU - {EB1974DC-D58F-41A8-A965-CC772F2B8CEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EB1974DC-D58F-41A8-A965-CC772F2B8CEB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EB1974DC-D58F-41A8-A965-CC772F2B8CEB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EB1974DC-D58F-41A8-A965-CC772F2B8CEB}.Release|Any CPU.Build.0 = Release|Any CPU - {5E16731E-1160-4F78-811C-037EE40C5AC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5E16731E-1160-4F78-811C-037EE40C5AC0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5E16731E-1160-4F78-811C-037EE40C5AC0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5E16731E-1160-4F78-811C-037EE40C5AC0}.Release|Any CPU.Build.0 = Release|Any CPU - {EA853C22-3BDD-43C0-A150-8C77BBE41058}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EA853C22-3BDD-43C0-A150-8C77BBE41058}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EA853C22-3BDD-43C0-A150-8C77BBE41058}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EA853C22-3BDD-43C0-A150-8C77BBE41058}.Release|Any CPU.Build.0 = Release|Any CPU - {8C2C5A8B-5CC4-48B3-8266-EBFB68DFC51C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8C2C5A8B-5CC4-48B3-8266-EBFB68DFC51C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8C2C5A8B-5CC4-48B3-8266-EBFB68DFC51C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8C2C5A8B-5CC4-48B3-8266-EBFB68DFC51C}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {E2990ADA-8398-4769-98E9-EA105130137D} = {6A878B68-ADCF-470F-8273-DA5C32E2DB79} - {3F9D43F2-9555-4771-A81B-9EBDE1A2C7CF} = {6A878B68-ADCF-470F-8273-DA5C32E2DB79} - {A1376FF6-1F7E-4D68-967A-121572141BA0} = {D499CDEC-C117-4968-A519-21C64757C640} - {EB1974DC-D58F-41A8-A965-CC772F2B8CEB} = {6A878B68-ADCF-470F-8273-DA5C32E2DB79} - {5E16731E-1160-4F78-811C-037EE40C5AC0} = {D499CDEC-C117-4968-A519-21C64757C640} - {EA853C22-3BDD-43C0-A150-8C77BBE41058} = {D499CDEC-C117-4968-A519-21C64757C640} - {8C2C5A8B-5CC4-48B3-8266-EBFB68DFC51C} = {6A878B68-ADCF-470F-8273-DA5C32E2DB79} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {3AD0BA6E-E14B-401F-B4F6-C7736B01643C} - EndGlobalSection -EndGlobal diff --git a/templates/docker-compose.yml b/templates/docker-compose.yml deleted file mode 100644 index 254211c6..00000000 --- a/templates/docker-compose.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: hexagonal-solution-template - -services: - sqlserver: - image: mcr.microsoft.com/mssql/server:2025-latest - environment: - SA_PASSWORD: "cY5VvZkkh4AzES" - ACCEPT_EULA: "Y" - MSSQL_PID: "Developer" - networks: - - hexagonal_solution_template_network - healthcheck: - test: /opt/mssql-tools18/bin/sqlcmd -U sa -P "cY5VvZkkh4AzES" -C -Q "SELECT 1" || exit 1 - interval: 3s - timeout: 10s - retries: 10 - start_period: 15s - ports: - - "1433:1433" - - mssqltools: - image: mcr.microsoft.com/mssql-tools - depends_on: - sqlserver: - condition: service_healthy - networks: - - hexagonal_solution_template_network - volumes: - - ./scripts/migrations.sql:/tmp/migrations.sql - - ./scripts/seeds.sql:/tmp/seeds.sql - command: - - /bin/bash - - -c - - | - ./opt/mssql-tools/bin/sqlcmd -S sqlserver -U sa -P "cY5VvZkkh4AzES" -C -d master -i ./tmp/migrations.sql && - ./opt/mssql-tools/bin/sqlcmd -S sqlserver -U sa -P "cY5VvZkkh4AzES" -C -d master -i ./tmp/seeds.sql - - aspire-dashboard: - image: mcr.microsoft.com/dotnet/aspire-dashboard:13 - ports: - - "18888:18888" - - "18889:18889" - environment: - - DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true - - redis: - image: redis:8 - ports: - - "6379:6379" - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 3s - timeout: 10s - retries: 5 - start_period: 15s - networks: - - hexagonal_solution_template_network - - rabbitmq: - image: rabbitmq:management - ports: - - "5672:5672" - - "15672:15672" - environment: - RABBITMQ_DEFAULT_USER: guest - RABBITMQ_DEFAULT_PASS: guest - healthcheck: - test: ["CMD", "rabbitmq-diagnostics", "ping"] - interval: 3s - timeout: 10s - retries: 5 - start_period: 15s - networks: - - hexagonal_solution_template_network - -networks: - hexagonal_solution_template_network: - driver: bridge diff --git a/templates/scripts/migrations.sql b/templates/scripts/migrations.sql deleted file mode 100644 index e1134f30..00000000 --- a/templates/scripts/migrations.sql +++ /dev/null @@ -1,73 +0,0 @@ -IF NOT EXISTS(SELECT * FROM sys.databases WHERE name = 'OrderDb') -BEGIN - CREATE DATABASE [OrderDb] -END -GO - -USE [OrderDb] -GO - - -IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL -BEGIN - CREATE TABLE [__EFMigrationsHistory] ( - [MigrationId] nvarchar(150) NOT NULL, - [ProductVersion] nvarchar(32) NOT NULL, - CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId]) - ); -END; -GO - -BEGIN TRANSACTION; -IF NOT EXISTS ( - SELECT * FROM [__EFMigrationsHistory] - WHERE [MigrationId] = N'20251012134409_AddNotificationTable' -) -BEGIN - CREATE TABLE [Notification] ( - [Id] int NOT NULL IDENTITY, - [NotificationType] varchar(100) NOT NULL, - [NotificationStatus] varchar(100) NOT NULL, - [Message] varchar(4000) NULL, - [CreatedAt] datetime2 NOT NULL, - [CreatedBy] varchar(100) NULL, - [UpdatedAt] datetime2 NOT NULL, - [UpdatedBy] varchar(100) NULL, - CONSTRAINT [PK_Notification] PRIMARY KEY ([Id]) - ); - - CREATE TABLE [Order] ( - [Id] int NOT NULL IDENTITY, - [Description] varchar(255) NOT NULL, - [Total] decimal(18,2) NOT NULL, - [CreatedAt] datetime2 NOT NULL, - [CreatedBy] varchar(100) NULL, - [UpdatedAt] datetime2 NOT NULL, - [UpdatedBy] varchar(100) NULL, - CONSTRAINT [PK_Order] PRIMARY KEY ([Id]) - ); - - CREATE TABLE [Item] ( - [Id] int NOT NULL IDENTITY, - [Name] varchar(200) NOT NULL, - [Description] varchar(255) NOT NULL, - [Value] decimal(18,2) NOT NULL, - [OrderId] int NULL, - [CreatedAt] datetime2 NOT NULL, - [CreatedBy] varchar(100) NULL, - [UpdatedAt] datetime2 NOT NULL, - [UpdatedBy] varchar(100) NULL, - CONSTRAINT [PK_Item] PRIMARY KEY ([Id]), - CONSTRAINT [FK_Item_Order_OrderId] FOREIGN KEY ([OrderId]) REFERENCES [Order] ([Id]) - ); - - CREATE INDEX [IX_Item_OrderId] ON [Item] ([OrderId]); - - INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) - VALUES (N'20251012134409_AddNotificationTable', N'9.0.8'); - -END; - -COMMIT; -GO - diff --git a/templates/scripts/seeds.sql b/templates/scripts/seeds.sql deleted file mode 100644 index c0c42ea0..00000000 --- a/templates/scripts/seeds.sql +++ /dev/null @@ -1,37 +0,0 @@ -USE [OrderDb] -GO - -SET IDENTITY_INSERT [Order] ON -GO - -IF NOT EXISTS (SELECT TOP 1 * FROM [Order] WHERE [Id] = 1) BEGIN - INSERT INTO [Order] ([Id], [Description], [Total], [CreatedAt], [UpdatedAt]) - VALUES (1, 'XPTO Client Computers', 1000.00, GETDATE(), GETDATE()); -END -GO - -SET IDENTITY_INSERT [Order] OFF -GO - -SET IDENTITY_INSERT [Item] ON -GO - -IF NOT EXISTS (SELECT TOP 1 * FROM [Item] WHERE [Id] = 1 ) BEGIN - INSERT INTO [Item] ([Id], [Name], [Description], [Value], [OrderId], [CreatedAt], [UpdatedAt]) - VALUES (1, 'Graphics Card 4090 Super', 'Nvidia Graphics Cards 24GB RX 4090 Super', 999.00, 1, GETDATE(), GETDATE()); -END -GO - -SET IDENTITY_INSERT [Item] OFF -GO - -SET IDENTITY_INSERT [Notification] ON - -IF NOT EXISTS (SELECT TOP 1 * FROM [Notification] WHERE [Id] = 1 ) BEGIN - INSERT INTO [Notification] ([Id], [NotificationType], [NotificationStatus], [CreatedAt], [UpdatedAt]) - VALUES (1, 'OrderCreated', 'Created', GETDATE(), GETDATE()); -END -GO - -SET IDENTITY_INSERT [Notification] OFF -GO diff --git a/templates/src/Application/Common/Constants/DefaultApplicationMessages.cs b/templates/src/Application/Common/Constants/DefaultApplicationMessages.cs deleted file mode 100644 index 8c72be68..00000000 --- a/templates/src/Application/Common/Constants/DefaultApplicationMessages.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Application.Common.Constants; - -public static class DefaultApplicationMessages -{ - public const string StartToExecuteUseCase = "[{ClassName}] | [{MethodName}] | [{CorrelationId}] | Start to execute use case"; - public const string FinishedExecutingUseCase = "[{ClassName}] | [{MethodName}] | [{CorrelationId}] | Elapsed time: {ElapsedMilliseconds} ms | Finished executing use case"; - public const string ValidationErrors = "[{ClassName}] | [{MethodName}] | [{CorrelationId}] | Validation errors: [{errors}]"; -} diff --git a/templates/src/Application/Common/Constants/DefaultConfigurations.cs b/templates/src/Application/Common/Constants/DefaultConfigurations.cs deleted file mode 100644 index fc90b5dd..00000000 --- a/templates/src/Application/Common/Constants/DefaultConfigurations.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Diagnostics.Metrics; - -namespace Application.Common.Constants; - -public static class DefaultConfigurations -{ - public static string ApplicationName => "Hexagonal.Solution.Template"; - public static readonly Meter Meter = new("Application"); - -} diff --git a/templates/src/Application/Common/Constants/NotificationType.cs b/templates/src/Application/Common/Constants/NotificationType.cs deleted file mode 100644 index d7b8c80b..00000000 --- a/templates/src/Application/Common/Constants/NotificationType.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Application.Common.Constants; - -public static class NotificationType -{ - public const string OrderCreated = nameof(OrderCreated); -} diff --git a/templates/src/Application/Common/Repositories/IBaseRepository.cs b/templates/src/Application/Common/Repositories/IBaseRepository.cs deleted file mode 100644 index 313ae5a3..00000000 --- a/templates/src/Application/Common/Repositories/IBaseRepository.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Linq.Expressions; -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 AddOrUpdateIfNotExistsAsync(TEntity entity, Expression> predicate, 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; - Task CheckExistsByWhereAsync(Expression> predicate, Guid correlationId, CancellationToken cancellationToken, bool? newContext = null) where TEntity : DomainEntity; - Task CheckExistsByWhereAsNoTrackingAsync(Expression> predicate, Guid correlationId, CancellationToken cancellationToken, bool? newContext = null) where TEntity : DomainEntity; - - Task FirstOrDefaultAsNoTrackingAsync( - Guid correlationId, - Expression> predicate, - CancellationToken cancellationToken, - bool? newContext = null, - params Expression>[]? includes - ) where TEntity : DomainEntity; - Task FirstOrDefaultAsNoTrackingAsync( - Guid correlationId, - Expression> predicate, - Expression> selector, - CancellationToken cancellationToken, - bool? newContext = null - ) where TEntity : DomainEntity; - Task GetByIdAsNoTrackingAsync( - int id, - Guid correlationId, - CancellationToken cancellationToken, - bool? newContext = null, - params Expression>[]? includes - ) where TEntity : DomainEntity; - Task GetByIdAsNoTrackingAsync( - int id, - Guid correlationId, - Expression> selector, - CancellationToken cancellationToken, - bool? newContext = null - ) where TEntity : DomainEntity; - Task> GetByWhereAsync( - Guid correlationId, - Expression> predicate, - CancellationToken cancellationToken, - bool? newContext = null, - params Expression>[]? includes - ) where TEntity : DomainEntity; - Task> GetByWhereAsNoTrackingAsync( - Guid correlationId, - Expression> predicate, - CancellationToken cancellationToken, - bool? newContext = null, - params Expression>[]? includes - ) where TEntity : DomainEntity; - Task> GetByWhereAsNoTrackingAsync( - Guid correlationId, - Expression> predicate, - Expression> selector, - CancellationToken cancellationToken, - bool? newContext = 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; - - Task BeginTransactionAsync(CancellationToken cancellationToken); - Task CommitTransactionAsync(CancellationToken cancellationToken); - Task RollbackTransactionAsync(CancellationToken cancellationToken); -} diff --git a/templates/src/Application/Common/UseCases/BaseInOutUseCase.cs b/templates/src/Application/Common/UseCases/BaseInOutUseCase.cs deleted file mode 100644 index 4b2edb9b..00000000 --- a/templates/src/Application/Common/UseCases/BaseInOutUseCase.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System.Diagnostics.Metrics; -using Application.Common.Constants; -using Application.Common.Repositories; -using Application.Common.Requests; -using Application.Common.Services; -using FluentValidation; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Application.Common.UseCases; - -public interface IBaseInOutUseCase - where TRequest : BaseRequest - where TResponseData : BaseResponse -{ - Task HandleAsync(TRequest request, CancellationToken cancellationToken); -} - -public abstract class BaseInOutUseCase : BaseUseCase, IBaseInOutUseCase - where TRequest : BaseRequest - where TResponseData : BaseResponse -{ - protected readonly IHybridCacheService _cache; - protected readonly IProduceService _produceService; - protected readonly IBaseRepository _repository; - private readonly IValidator _validator; - private readonly Histogram _useCaseExecuted; - private readonly Gauge _useCaseExecutionElapsedTime; - protected const string HandleMethodName = nameof(HandleAsync); - - protected BaseInOutUseCase(IServiceProvider serviceProvider) : base(serviceProvider) - { - _cache = serviceProvider.GetRequiredService(); - _produceService = serviceProvider.GetRequiredService(); - _repository = serviceProvider.GetRequiredService(); - _validator = serviceProvider.GetRequiredService>(); - - _useCaseExecuted = DefaultConfigurations.Meter - .CreateHistogram($"{ClassName}.Executed", "total", "Number of times the use case was executed"); - - _useCaseExecutionElapsedTime = DefaultConfigurations.Meter - .CreateGauge($"{ClassName}.Elapsed", "elapsed", "Elapsed time taken to execute the use case"); - } - - public async Task HandleAsync( - TRequest request, - CancellationToken cancellationToken - ) - { - stopWatch.Restart(); - - logger.LogInformation( - DefaultApplicationMessages.StartToExecuteUseCase, - ClassName, HandleMethodName, request.CorrelationId - ); - TResponseData response; - - if (_validator != null) - { - var validationResult = await _validator.ValidateAsync(request, cancellationToken); - if (!validationResult.IsValid) - { - string errors = string.Join(", ", validationResult.Errors); - logger.LogError( - DefaultApplicationMessages.ValidationErrors, - ClassName, HandleMethodName, request.CorrelationId, errors - ); - - response = Activator.CreateInstance(); - response = response with - { - Success = false, - Message = errors - }; - - return response!; - } - } - - response = await HandleInternalAsync(request, cancellationToken); - - logger.LogInformation( - DefaultApplicationMessages.FinishedExecutingUseCase, - ClassName, HandleMethodName, request.CorrelationId, stopWatch.ElapsedMilliseconds - ); - - _useCaseExecuted.Record(1); - _useCaseExecutionElapsedTime.Record(stopWatch.ElapsedMilliseconds); - - return response; - } - - public abstract Task HandleInternalAsync(TRequest request, CancellationToken cancellationToken); -} diff --git a/templates/src/Application/Common/UseCases/BaseInUseCase.cs b/templates/src/Application/Common/UseCases/BaseInUseCase.cs deleted file mode 100644 index 5d971497..00000000 --- a/templates/src/Application/Common/UseCases/BaseInUseCase.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Application.Common.Requests; -using FluentValidation; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Application.Common.Services; -using Application.Common.Constants; -using System.Diagnostics.Metrics; -using Application.Common.Repositories; - -namespace Application.Common.UseCases; - -public interface IBaseInUseCase where TRequest : BaseRequest -{ - Task HandleAsync(TRequest request, CancellationToken cancellationToken); -} - -public abstract class BaseInUseCase : BaseUseCase, IBaseInUseCase where TRequest : BaseRequest -{ - protected readonly IHybridCacheService _cache; - protected readonly IProduceService _produceService; - protected readonly IBaseRepository _repository; - private readonly IValidator _validator; - private readonly Histogram _useCaseExecuted; - private readonly Gauge _useCaseExecutionElapsedTime; - protected const string HandleMethodName = nameof(HandleAsync); - - protected BaseInUseCase(IServiceProvider serviceProvider) : base(serviceProvider) - { - _cache = serviceProvider.GetRequiredService(); - _produceService = serviceProvider.GetRequiredService(); - _repository = serviceProvider.GetRequiredService(); - _validator = serviceProvider.GetRequiredService>(); - - _useCaseExecuted = DefaultConfigurations.Meter - .CreateHistogram($"{ClassName}.Executed", "total", "Number of times the use case was executed"); - - _useCaseExecutionElapsedTime = DefaultConfigurations.Meter - .CreateGauge($"{ClassName}.Elapsed", "elapsed", "Elapsed time taken to execute the use case"); - } - - public async Task HandleAsync( - TRequest request, - CancellationToken cancellationToken - ) - { - stopWatch.Restart(); - - logger.LogInformation( - DefaultApplicationMessages.StartToExecuteUseCase, - ClassName, HandleMethodName, request.CorrelationId - ); - - if (_validator != null) - { - var validationResult = await _validator.ValidateAsync(request, cancellationToken); - if (!validationResult.IsValid) - { - var errors = string.Join(", ", validationResult.Errors); - logger.LogError( - DefaultApplicationMessages.ValidationErrors, - ClassName, HandleMethodName, request.CorrelationId, errors - ); - - return; - } - } - - await HandleInternalAsync(request, cancellationToken); - - logger.LogInformation( - DefaultApplicationMessages.FinishedExecutingUseCase, - ClassName, HandleMethodName, request.CorrelationId, stopWatch.ElapsedMilliseconds - ); - - _useCaseExecuted.Record(1); - _useCaseExecutionElapsedTime.Record(stopWatch.ElapsedMilliseconds); - } - - public abstract Task HandleInternalAsync(TRequest request, CancellationToken cancellationToken); -} diff --git a/templates/src/Application/Common/UseCases/BaseOutUseCase.cs b/templates/src/Application/Common/UseCases/BaseOutUseCase.cs deleted file mode 100644 index 2832e492..00000000 --- a/templates/src/Application/Common/UseCases/BaseOutUseCase.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Diagnostics.Metrics; -using Application.Common.Constants; -using Application.Common.Requests; -using Application.Common.Services; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Application.Common.UseCases; - -public interface IBaseOutUseCase where TResponseData : BaseResponse -{ - Task HandleAsync(CancellationToken cancellationToken); -} - -public abstract class BaseOutUseCase : BaseUseCase, IBaseOutUseCase where TResponseData : BaseResponse -{ - protected readonly IHybridCacheService _cache; - protected readonly IProduceService _produceService; - private readonly Histogram _useCaseExecuted; - private readonly Gauge _useCaseExecutionElapsedTime; - protected const string HandleMethodName = nameof(HandleAsync); - - protected BaseOutUseCase(IServiceProvider serviceProvider) : base(serviceProvider) - { - _cache = serviceProvider.GetRequiredService(); - _produceService = serviceProvider.GetRequiredService(); - - _useCaseExecuted = DefaultConfigurations.Meter - .CreateHistogram($"{ClassName}.Executed", "total", "Number of times the use case was executed"); - - _useCaseExecutionElapsedTime = DefaultConfigurations.Meter - .CreateGauge($"{ClassName}.Elapsed", "elapsed", "Elapsed time taken to execute the use case"); - } - - public async Task HandleAsync(CancellationToken cancellationToken) - { - stopWatch.Restart(); - var correlationId = Guid.NewGuid(); - logger.LogInformation(DefaultApplicationMessages.StartToExecuteUseCase, ClassName, HandleMethodName, correlationId); - - var response = await HandleInternalAsync(cancellationToken); - - logger.LogInformation( - DefaultApplicationMessages.FinishedExecutingUseCase, - ClassName, HandleMethodName, correlationId, stopWatch.ElapsedMilliseconds - ); - - _useCaseExecuted.Record(1); - _useCaseExecutionElapsedTime.Record(stopWatch.ElapsedMilliseconds); - - return response; - } - - public abstract Task HandleInternalAsync(CancellationToken cancellationToken); -} diff --git a/templates/src/Application/Common/UseCases/BaseUseCase.cs b/templates/src/Application/Common/UseCases/BaseUseCase.cs deleted file mode 100644 index cb836050..00000000 --- a/templates/src/Application/Common/UseCases/BaseUseCase.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Diagnostics; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Application.Common.UseCases; - -public abstract class BaseUseCase -{ - protected readonly IServiceProvider serviceProvider; - protected readonly ILogger logger; - protected readonly Stopwatch stopWatch = new(); - protected string ClassName; - - protected BaseUseCase(IServiceProvider serviceProvider) - { - var classType = GetType(); - ClassName = classType.Name; - - this.serviceProvider = serviceProvider; - - logger = serviceProvider.GetRequiredService().CreateLogger(classType); - } -} \ No newline at end of file diff --git a/templates/src/Domain/Common/DomainEntity.cs b/templates/src/Domain/Common/DomainEntity.cs deleted file mode 100644 index fdead773..00000000 --- a/templates/src/Domain/Common/DomainEntity.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Domain.Common; - -public abstract class DomainEntity -{ - protected DomainEntity() {} - protected DomainEntity(DateTime currentDate, string? user = null) - { - CreatedAt = currentDate; - CreatedBy = user ?? "System"; - UpdatedAt = currentDate; - UpdatedBy = user ?? "System"; - } - - public int Id { get; init; } - public DateTime CreatedAt { get; init; } - public string? CreatedBy { get; init; } - public DateTime UpdatedAt { get; private set; } - public string? UpdatedBy { get; private set; } - - public virtual void Update(string? user = null) - { - UpdatedAt = DateTime.UtcNow; - UpdatedBy = user ?? "System"; - } -} diff --git a/templates/src/Domain/Orders/Order.cs b/templates/src/Domain/Orders/Order.cs deleted file mode 100644 index 1a9a7c09..00000000 --- a/templates/src/Domain/Orders/Order.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Domain.Common; - -namespace Domain.Orders; - -public sealed class Order : DomainEntity -{ - public Order() { } - - public Order(string description, ICollection items) : base(DateTime.UtcNow) - { - Description = description; - Items = items; - } - - public string Description { get; private set; } - public decimal Total { get; private set; } - public ICollection Items { get; private set; } - - public Result SetTotal() - { - if (Items == null || Items.Count == 0) - return Result.Fail("Order must have at least one item."); - - Total = Items.Sum(item => item.Value); - Update(); - - return Result.Ok(); - } -} diff --git a/templates/src/Infrastructure/Cache/Services/HybridCacheService.cs b/templates/src/Infrastructure/Cache/Services/HybridCacheService.cs deleted file mode 100644 index 5a8d451f..00000000 --- a/templates/src/Infrastructure/Cache/Services/HybridCacheService.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Application.Common.Constants; -using Application.Common.Services; -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; - - public async ValueTask GetOrCreateAsync( - string key, - Func> factory, - CancellationToken cancellationToken - ) - { - _logger.LogDebug("[HybridCacheService] | [GetOrCreateAsync] | [{Key}] | Retrieving cache entry", key); - - var result = await _cache.GetOrCreateAsync($"{DefaultConfigurations.ApplicationName}:{key}", factory, cancellationToken: cancellationToken); - - _logger.LogDebug("[HybridCacheService] | [GetOrCreateAsync] | [{Key}] | Cache entry retrieved", key); - - return result; - } - - public async ValueTask CreateAsync(string key, TResult value, CancellationToken cancellationToken) - { - _logger.LogDebug("[HybridCacheService] | [CreateAsync] | [{Key}] | Creating cache entry", key); - - await _cache.SetAsync($"{DefaultConfigurations.ApplicationName}:{key}", value, cancellationToken: cancellationToken); - - _logger.LogDebug("[HybridCacheService] | [CreateAsync] | [{Key}] | Cache entry created", key); - } - - public async ValueTask DeleteAsync(string key, CancellationToken cancellationToken) - { - _logger.LogDebug("[HybridCacheService] | [DeleteAsync] | [{Key}] | Deleting cache entry", key); - - await _cache.RemoveAsync($"{DefaultConfigurations.ApplicationName}:{key}", cancellationToken); - - _logger.LogDebug("[HybridCacheService] | [DeleteAsync] | [{Key}] | Cache entry deleted", key); - } -} diff --git a/templates/src/Infrastructure/Data/Common/BaseRepository.cs b/templates/src/Infrastructure/Data/Common/BaseRepository.cs deleted file mode 100644 index 024d491f..00000000 --- a/templates/src/Infrastructure/Data/Common/BaseRepository.cs +++ /dev/null @@ -1,334 +0,0 @@ -using System.Diagnostics; -using System.Linq.Expressions; -using System.Runtime.CompilerServices; -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 -{ - protected readonly ILogger logger = logger; - private readonly Stopwatch _stopwatch = new(); - private readonly IDbContextFactory _dbContextFactory = dbContextFactory; - private readonly MyDbContext _dbContext = dbContextFactory.CreateDbContext(); - - private async Task HandleBaseQueryAsync( - Func, Task> query, - Guid correlationId, - bool? newContext = false, - [CallerMemberName] - string methodName = null! - ) where TEntity : DomainEntity - { - _stopwatch.Restart(); - - logger.LogDebug( - "[BaseRepository] | [{Method}] | CorrelationId: {CorrelationId} | Starting database operation.", - methodName, - correlationId - ); - - var dbSet = _dbContext.Set(); - if (newContext.GetValueOrDefault()) - dbSet = _dbContextFactory.CreateDbContext().Set(); - - var result = await query.Invoke(dbSet); - - logger.LogDebug( - "[BaseRepository] | [{Method}] | CorrelationId: {CorrelationId} | Query executed in {ElapsedMilliseconds} ms.", - methodName, - correlationId, - _stopwatch.ElapsedMilliseconds - ); - - return result; - } - - 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 AddOrUpdateIfNotExistsAsync(TEntity entity, Expression> predicate, Guid correlationId, CancellationToken cancellationToken, bool? newContext = null) where TEntity : DomainEntity => - await HandleBaseQueryAsync(async dbEntitySet => - { - if (!await dbEntitySet.AsNoTracking().AnyAsync(predicate)) - await dbEntitySet.AddAsync(entity, cancellationToken); - else - dbEntitySet.Update(entity); - - 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 CheckExistsByWhereAsync(Expression> predicate, Guid correlationId, CancellationToken cancellationToken, bool? newContext = null) where TEntity : DomainEntity => - await HandleBaseQueryAsync(async dbEntitySet => - { - return await dbEntitySet.AnyAsync(predicate, cancellationToken); - }, correlationId, newContext); - - public async Task CheckExistsByWhereAsNoTrackingAsync(Expression> predicate, Guid correlationId, CancellationToken cancellationToken, bool? newContext = null) where TEntity : DomainEntity => - await HandleBaseQueryAsync(async dbEntitySet => - { - return await dbEntitySet.AsNoTracking().AnyAsync(predicate, cancellationToken); - }, correlationId, newContext); - public async Task GetByIdAsNoTrackingAsync( - int id, - Guid correlationId, - CancellationToken cancellationToken, - bool? newContext = null, - params Expression>[]? includes - ) where TEntity : DomainEntity => await HandleBaseQueryAsync(async dbEntitySet => - { - var query = dbEntitySet.AsNoTracking(); - - if (includes is not null) - query = SetIncludes(includes, query); - - return await query.FirstOrDefaultAsync(o => o.Id == id, cancellationToken) ?? default!; - }, correlationId, newContext); - - public async Task GetByIdAsNoTrackingAsync( - int id, - Guid correlationId, - Expression> selector, - CancellationToken cancellationToken, - bool? newContext = null - ) where TEntity : DomainEntity => await HandleBaseQueryAsync(async dbEntitySet => await dbEntitySet - .Where(o => o.Id == id) - .Select(selector) - .FirstOrDefaultAsync(cancellationToken) ?? default!, - correlationId, - newContext - ); - - public async Task> GetByWhereAsync( - Guid correlationId, - Expression> predicate, - CancellationToken cancellationToken, - bool? newContext = null, - params Expression>[]? includes - ) where TEntity : DomainEntity => await HandleBaseQueryAsync>(async dbEntitySet => - { - var query = dbEntitySet.AsQueryable(); - - if (includes is not null) - query = SetIncludes(includes, query); - - return await query.Where(predicate).ToListAsync(cancellationToken); - }, correlationId, newContext); - - public async Task> GetByWhereAsNoTrackingAsync( - Guid correlationId, - Expression> predicate, - CancellationToken cancellationToken, - bool? newContext = null, - params Expression>[]? includes - ) where TEntity : DomainEntity => await HandleBaseQueryAsync>(async dbEntitySet => - { - var query = dbEntitySet.AsNoTracking(); - - if (includes is not null) - query = SetIncludes(includes, query); - - return await query.Where(predicate).ToListAsync(cancellationToken); - }, correlationId, newContext); - - public async Task> GetByWhereAsNoTrackingAsync( - Guid correlationId, - Expression> predicate, - Expression> selector, - CancellationToken cancellationToken, - bool? newContext = null - ) where TEntity : DomainEntity => await HandleBaseQueryAsync>(async dbEntitySet => - await dbEntitySet.Where(predicate).Select(selector).ToListAsync(cancellationToken), - correlationId, - newContext - ); - - public async Task FirstOrDefaultAsNoTrackingAsync( - Guid correlationId, - Expression> predicate, - CancellationToken cancellationToken, - bool? newContext = null, - params Expression>[]? includes - ) where TEntity : DomainEntity => await HandleBaseQueryAsync(async dbEntitySet => - { - var query = dbEntitySet.AsNoTracking(); - - if (includes is not null) - query = SetIncludes(includes, query); - - return await query.FirstOrDefaultAsync(predicate, cancellationToken) ?? default!; - }, correlationId, newContext); - - public async Task FirstOrDefaultAsNoTrackingAsync( - Guid correlationId, - Expression> predicate, - Expression> selector, - CancellationToken cancellationToken, - bool? newContext = null - ) where TEntity : DomainEntity => await HandleBaseQueryAsync(async dbEntitySet => await dbEntitySet - .Where(predicate) - .Select(selector) - .FirstOrDefaultAsync(cancellationToken) ?? default!, - 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(e, sortBy)) - : query.OrderBy(e => EF.Property(e, sortBy)); - else - query = query.OrderBy(e => e.CreatedAt); - - var totalRecords = await query.CountAsync(cancellationToken); - - if (searchByValues != null && searchByValues.Count != 0) - foreach (var searchByValue in searchByValues) - query = query.Where(e => - EF.Property(e, searchByValue.Key).Contains(searchByValue.Value.ToLowerInvariant()) - ); - - var items = await query - .Skip((page - 1) * pageSize) - .Take(pageSize) - .ToListAsync(cancellationToken); - - return (items, totalRecords); - }, correlationId, newContext); - - public async 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 => await HandleBaseQueryAsync Items, int TotalRecords)>(async dbEntitySet => - { - var query = dbEntitySet.AsQueryable(); - - if (predicate != null) - query = query.Where(predicate); - - if (!string.IsNullOrWhiteSpace(sortBy)) - query = sortDescending - ? query.OrderByDescending(e => EF.Property(e, sortBy)) - : query.OrderBy(e => EF.Property(e, sortBy)); - else - query = query.OrderBy(e => e.CreatedAt); - - var totalRecords = await query.CountAsync(cancellationToken); - - if (searchByValues != null && searchByValues.Count != 0) - foreach (var searchByValue in searchByValues) - query = query.Where(e => - EF.Property(e, searchByValue.Key).Contains(searchByValue.Value.ToLowerInvariant()) - ); - - var items = await query - .Skip((page - 1) * pageSize) - .Take(pageSize) - .Select(selector) - .ToListAsync(cancellationToken); - - return (items, totalRecords); - }, correlationId, newContext); - - public async Task BeginTransactionAsync(CancellationToken cancellationToken) - { - if (_dbContext.Database.CurrentTransaction == null) - await _dbContext.Database.BeginTransactionAsync(cancellationToken); - } - - public async Task CommitTransactionAsync(CancellationToken cancellationToken) - { - if (_dbContext.Database.CurrentTransaction != null) - { - _dbContext.SaveChanges(); - await _dbContext.Database.CurrentTransaction.CommitAsync(cancellationToken); - } - } - - public async Task RollbackTransactionAsync(CancellationToken cancellationToken) - { - if (_dbContext.Database.CurrentTransaction != null) - await _dbContext.Database.CurrentTransaction.RollbackAsync(cancellationToken); - } - - private static IQueryable SetIncludes( - Expression>[] includes, - IQueryable query - ) where TEntity : DomainEntity - { - if (includes != null && includes.Length > 0) - foreach (var include in includes) - query = query.Include(include); - - return query; - } -} diff --git a/templates/src/Infrastructure/Data/Migrations/20251012134409_AddNotificationTable.cs b/templates/src/Infrastructure/Data/Migrations/20251012134409_AddNotificationTable.cs deleted file mode 100644 index d00d9ef1..00000000 --- a/templates/src/Infrastructure/Data/Migrations/20251012134409_AddNotificationTable.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Infrastructure.Data.Migrations -{ - /// - public partial class AddNotificationTable : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Notification", - columns: table => new - { - Id = table.Column(type: "int", nullable: false) - .Annotation("SqlServer:Identity", "1, 1"), - NotificationType = table.Column(type: "varchar(100)", maxLength: 100, nullable: false), - NotificationStatus = table.Column(type: "varchar(100)", maxLength: 100, nullable: false), - Message = table.Column(type: "varchar(4000)", maxLength: 4000, nullable: true), - CreatedAt = table.Column(type: "datetime2", nullable: false), - CreatedBy = table.Column(type: "varchar(100)", maxLength: 100, nullable: true), - UpdatedAt = table.Column(type: "datetime2", nullable: false), - UpdatedBy = table.Column(type: "varchar(100)", maxLength: 100, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Notification", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Order", - columns: table => new - { - Id = table.Column(type: "int", nullable: false) - .Annotation("SqlServer:Identity", "1, 1"), - Description = table.Column(type: "varchar(255)", maxLength: 255, nullable: false), - Total = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), - CreatedAt = table.Column(type: "datetime2", nullable: false), - CreatedBy = table.Column(type: "varchar(100)", maxLength: 100, nullable: true), - UpdatedAt = table.Column(type: "datetime2", nullable: false), - UpdatedBy = table.Column(type: "varchar(100)", maxLength: 100, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Order", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Item", - columns: table => new - { - Id = table.Column(type: "int", nullable: false) - .Annotation("SqlServer:Identity", "1, 1"), - Name = table.Column(type: "varchar(200)", maxLength: 200, nullable: false), - Description = table.Column(type: "varchar(255)", maxLength: 255, nullable: false), - Value = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), - OrderId = table.Column(type: "int", nullable: true), - CreatedAt = table.Column(type: "datetime2", nullable: false), - CreatedBy = table.Column(type: "varchar(100)", maxLength: 100, nullable: true), - UpdatedAt = table.Column(type: "datetime2", nullable: false), - UpdatedBy = table.Column(type: "varchar(100)", maxLength: 100, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Item", x => x.Id); - table.ForeignKey( - name: "FK_Item_Order_OrderId", - column: x => x.OrderId, - principalTable: "Order", - principalColumn: "Id"); - }); - - migrationBuilder.CreateIndex( - name: "IX_Item_OrderId", - table: "Item", - column: "OrderId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Item"); - - migrationBuilder.DropTable( - name: "Notification"); - - migrationBuilder.DropTable( - name: "Order"); - } - } -} diff --git a/templates/src/Infrastructure/OpenTelemetry/InfrastructureOpenTelemetryDependencyInjection.cs b/templates/src/Infrastructure/OpenTelemetry/InfrastructureOpenTelemetryDependencyInjection.cs deleted file mode 100644 index 9adf6e8c..00000000 --- a/templates/src/Infrastructure/OpenTelemetry/InfrastructureOpenTelemetryDependencyInjection.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Application.Common.Constants; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using OpenTelemetry.Exporter; -using OpenTelemetry.Logs; -using OpenTelemetry.Metrics; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; - -namespace Infrastructure.OpenTelemetry; - -internal static class InfrastructureOpenTelemetryDependencyInjection -{ - extension(WebApplicationBuilder builder) - { - public WebApplicationBuilder AddOpenTelemetry() - { - var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); - var exporterProtocol = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_PROTOCOL")?.ToLower() == "grpc" - ? OtlpExportProtocol.Grpc - : OtlpExportProtocol.HttpProtobuf; - var exporterMetricsEndpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"); - var exporterTracesEndpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"); - var exporterLogsEndpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"); - - if ( - string.Equals(environment, "IntegrationTests", StringComparison.OrdinalIgnoreCase) || - string.IsNullOrWhiteSpace(exporterLogsEndpoint) || - string.IsNullOrWhiteSpace(exporterMetricsEndpoint) || - string.IsNullOrWhiteSpace(exporterTracesEndpoint) - ) - { - return builder; - } - - builder.Services.AddOpenTelemetry() - .ConfigureResource(resource => resource.AddEnvironmentVariableDetector()) - .WithMetrics(metrics => metrics - .AddAspNetCoreInstrumentation() - .AddMeter(DefaultConfigurations.Meter.Name) - .AddHttpClientInstrumentation() - .AddOtlpExporter(options => - { - options.Protocol = exporterProtocol; - options.Endpoint = new Uri(exporterMetricsEndpoint); - }) - ) - .WithTracing(tracing => tracing - .AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation(options => - { - options.RecordException = true; - }) - .AddEntityFrameworkCoreInstrumentation( - options => - { - options.SetDbStatementForText = true; - options.SetDbStatementForStoredProcedure = true; - } - ) - .AddRedisInstrumentation() - .AddRabbitMQInstrumentation() - .AddGrpcClientInstrumentation() - .AddOtlpExporter(options => - { - options.Protocol = exporterProtocol; - options.Endpoint = new Uri(exporterTracesEndpoint!); - }) - ) - .WithLogging(logging => logging - .AddOtlpExporter(options => - { - options.Protocol = exporterProtocol; - options.Endpoint = new Uri(exporterLogsEndpoint!); - }) - ); - - builder.Services.AddLogging(logging => logging.AddOpenTelemetry(openTelemetryLoggerOptions => - { - openTelemetryLoggerOptions.IncludeScopes = true; - openTelemetryLoggerOptions.IncludeFormattedMessage = true; - })); - - return builder; - } - } -} diff --git a/templates/src/WebApp/Properties/launchSettings.json b/templates/src/WebApp/Properties/launchSettings.json deleted file mode 100644 index 58992051..00000000 --- a/templates/src/WebApp/Properties/launchSettings.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "profiles": { - "Development": { - "commandName": "Project", - "dotnetRunMessages": true, - "sqlDebugging": true, - "launchBrowser": false, - "applicationUrl": "https://*:7175;http://*:5010", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "LOGGING__LOGLEVEL__DEFAULT": "Debug", - "LOGGING__LOGLEVEL__MICROSOFT": "Information", - "LOGGING__LOGLEVEL__MICROSOFT_HOSTING_LIFETIME": "Information", - "LOGGING__LOGLEVEL__MICROSOFT_ENTITY_FRAMEWORK_CORE": "Information", - "LOGGING__LOGLEVEL__MICROSOFT_ENTITY_FRAMEWORK_CORE_DATABASE_COMMAND": "Information", - "ENABLE_SENSITIVE_DATA_LOGGING": "true", - "OTEL_SERVICE_NAME": "Hexagonal.Solution.Template.WebApp", - "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:18889", - "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://localhost:18889", - "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "http://localhost:18889", - "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "http://localhost:18889", - "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", - "OTEL_RESOURCE_ATTRIBUTES": "service.namespace=development", - "ConnectionStrings__OrderDb": "Server=127.0.0.1,1433;Database=OrderDb;User Id=sa;Password=cY5VvZkkh4AzES;TrustServerCertificate=true;", - "ConnectionStrings__Redis": "127.0.0.1:6379", - "ConnectionStrings__RabbitMQ": "amqp://guest:guest@127.0.0.1:5672/" - - } - } - } -} diff --git a/templates/src/WebApp/appsettings.json b/templates/src/WebApp/appsettings.json deleted file mode 100644 index 770f1bb3..00000000 --- a/templates/src/WebApp/appsettings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ConnectionStrings": { - "OrderDb": "Server=127.0.0.1,1433;Database=OrderDb;User Id=sa;Password=cY5VvZkkh4AzES;TrustServerCertificate=true;", - "Redis": "localhost:6379", - "RabbitMq": "amqp://guest:guest@localhost:5672/" - } -} \ No newline at end of file diff --git a/templates/tests/CommonTests/Fixtures/BaseFixture.cs b/templates/tests/CommonTests/Fixtures/BaseFixture.cs deleted file mode 100644 index 0b8da73e..00000000 --- a/templates/tests/CommonTests/Fixtures/BaseFixture.cs +++ /dev/null @@ -1,14 +0,0 @@ -using AutoFixture; - -namespace CommonTests.Fixtures; -public class BaseFixture -{ - public Fixture autoFixture = new(); - - public CancellationToken cancellationToken = CancellationToken.None; - - public BaseFixture() - { - autoFixture.Behaviors.Add(new OmitOnRecursionBehavior()); - } -} diff --git a/templates/tests/UnitTests/Application/Orders/GetOrderUseCaseTests.cs b/templates/tests/UnitTests/Application/Orders/GetOrderUseCaseTests.cs deleted file mode 100644 index 998ec930..00000000 --- a/templates/tests/UnitTests/Application/Orders/GetOrderUseCaseTests.cs +++ /dev/null @@ -1,137 +0,0 @@ -using Application.Orders; -using Domain.Orders; -using Microsoft.Extensions.Logging; -using UnitTests.Application.Common; - -namespace UnitTests.Application.Orders; - -public sealed class GetOrderUseCaseFixture : BaseApplicationFixture -{ - public GetOrderUseCaseFixture() - { - useCase = new(mockServiceProvider.Object); - } - public GetOrderRequest SetValidRequest() => new(Guid.NewGuid(), autoFixture.Create()); - - public void VerifyOrderNotFoundLog(int times = 1) => - mockLogger.VerifyLog(l => l.LogWarning("*Order not found.*"), Times.Exactly(times)); -} - -public sealed class GetOrderUseCaseTest : IClassFixture -{ - private readonly GetOrderUseCaseFixture _fixture; - - public GetOrderUseCaseTest(GetOrderUseCaseFixture fixture) - { - _fixture = fixture; - _fixture.ClearInvocations(); - } - - [Fact(DisplayName = nameof(Given_A_Valid_Request_Then_Pass))] - public async Task Given_A_Valid_Request_Then_Pass() - { - // Arrange - var request = _fixture.SetValidRequest(); - _fixture.SetSuccessfulValidator(request); - var expectedOrder = _fixture.autoFixture.Create(); - _fixture.mockRepository.SetupGetByIdAsNoTrackingAsync(expectedOrder); - - // Act - var result = await _fixture.useCase.HandleAsync(request, _fixture.cancellationToken); - - // Assert - Assert.NotNull(result); - Assert.True(result.Success); - Assert.Null(result.Message); - Assert.NotNull(result.Data); - Assert.Equal(expectedOrder.Id, result.Data.Id); - Assert.Equal(expectedOrder.Description, result.Data.Description); - Assert.Equal(expectedOrder.Total, result.Data.Total); - Assert.NotNull(result.Data.Items); - Assert.Equal(expectedOrder.Items?.Count, result.Data.Items!.Count); - - _fixture.VerifyStartUseCaseLog(); - _fixture.VerifyOrderNotFoundLog(0); - _fixture.VerifyFinishUseCaseLog(); - } - - [Fact(DisplayName = nameof(Given_A_Valid_Request_Without_Items_Then_Pass))] - public async Task Given_A_Valid_Request_Without_Items_Then_Pass() - { - // Arrange - var request = _fixture.SetValidRequest(); - _fixture.SetSuccessfulValidator(request); - OrderDto expectedOrder = new() - { - Id = 1, - Description = "Test Order", - Total = 1000m, - Items = [] - }; - _fixture.mockRepository.SetupGetByIdAsNoTrackingAsync(expectedOrder); - - // Act - var result = await _fixture.useCase.HandleAsync(request, _fixture.cancellationToken); - - // Assert - Assert.NotNull(result); - Assert.True(result.Success); - Assert.Null(result.Message); - Assert.NotNull(result.Data); - Assert.Equal(expectedOrder.Id, result.Data.Id); - Assert.Equal(expectedOrder.Description, result.Data.Description); - Assert.Equal(expectedOrder.Total, result.Data.Total); - Assert.Equal(0, result.Data.Items?.Count); - - _fixture.VerifyStartUseCaseLog(); - _fixture.VerifyOrderNotFoundLog(0); - _fixture.VerifyFinishUseCaseLog(); - } - - [Fact(DisplayName = nameof(Given_A_Invalid_Request_Then_Fails))] - public async Task Given_A_Invalid_Request_Then_Fails() - { - // Arrange - var request = _fixture.SetValidRequest(); - _fixture.SetFailedValidator(request); - - // Act - var result = await _fixture.useCase.HandleAsync( - request, - _fixture.cancellationToken - ); - - // Assert - Assert.False(result.Success); - Assert.NotNull(result.Message); - Assert.NotEmpty(result.Message); - - _fixture.VerifyStartUseCaseLog(); - _fixture.VerifyOrderNotFoundLog(0); - _fixture.VerifyFinishUseCaseLog(0); - } - - [Fact(DisplayName = nameof(Given_A_Valid_Request_When_Order_Not_Found_Then_Fails))] - public async Task Given_A_Valid_Request_When_Order_Not_Found_Then_Fails() - { - // Arrange - var request = _fixture.SetValidRequest(); - _fixture.SetSuccessfulValidator(request); - - // Act - var result = await _fixture.useCase.HandleAsync( - request, - _fixture.cancellationToken - ); - - // Assert - Assert.False(result.Success); - Assert.NotNull(result.Message); - Assert.NotEmpty(result.Message); - Assert.Equal("Order not found.", result.Message); - - _fixture.VerifyStartUseCaseLog(); - _fixture.VerifyOrderNotFoundLog(1); - _fixture.VerifyFinishUseCaseLog(); - } -} diff --git a/templates/tests/UnitTests/Domain/OrderTests.cs b/templates/tests/UnitTests/Domain/OrderTests.cs deleted file mode 100644 index 4c5ea250..00000000 --- a/templates/tests/UnitTests/Domain/OrderTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Domain.Orders; - -namespace UnitTests.Domain; - -public sealed class OrderTests -{ - [Fact(DisplayName = nameof(Given_A_New_Order_When_Items_Are_Provided_Then_Should_Set_Total_With_Success))] - public void Given_A_New_Order_When_Items_Are_Provided_Then_Should_Set_Total_With_Success() - { - /// Arrange - var items = new List() - { - new("Computer", "Desktop", 900), - new("Mouse", "Razer", 100), - new("Headphone", "Logitech", 100), - }; - Order order = new("Amazing Computer", items); - var initialUpdatedAt = order.UpdatedAt; - - /// Act - var result = order.SetTotal(); - - // Assert - Assert.NotNull(order); - Assert.NotNull(result); - Assert.True(result.Success); - Assert.Empty(result.Message); - Assert.NotEqual(0, order.Total); - Assert.NotEqual(initialUpdatedAt, order.UpdatedAt); - Assert.Equal(items.Sum(i => i.Value), order.Total); - } - - [Fact(DisplayName = nameof(Given_A_New_Order_When_Items_Is_Empty_Then_Should_Return_Failure))] - public void Given_A_New_Order_When_Items_Is_Empty_Then_Should_Return_Failure() - { - /// Arrange - Order order = new("Amazing Computer", Array.Empty()); - - /// Act - var result = order.SetTotal(); - - // Assert - Assert.NotNull(order); - Assert.NotNull(result); - Assert.True(result.IsFailure); - Assert.Equal("Order must have at least one item.", result.Message); - } -}